summaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-03 15:20:52 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-03 15:20:52 +0200
commit0b3ffdb8d7f07aee817ece24fd44a24ac60e6714 (patch)
tree4f65c9bb9128abf18f7739a711c77cfc8012fc06 /packages
parentc1be7571973c3d0b671ed4db2da41266ae3099d8 (diff)
downloadmeshbay-0b3ffdb8d7f07aee817ece24fd44a24ac60e6714.tar.gz
refactor(hub): drop the base64 chunk fallback and the dead HTTP file client
With MNP 0.15 no node can emit a base64 `file_chunk`, so the browser's fallback for that shape is unreachable. Three things go with it: - `file-utils.js` kept a third branch below the fallback that base64-decoded `chunkMsg.ct_b64 || chunkMsg.data_b64` when neither was present, i.e. decoded `undefined` and wrote the result into the file the user was saving. A chunk we cannot decrypt now stops the download with an error naming the file and suggesting the node is older than the page. Deliberately not in `_isRetryableTransportError`: this is a version mismatch, not a bad moment on the link. - `crypto.js` `decryptChunk` (base64) was the real path until Phase 9.15 and has had no caller since. - `crypto.js` `decryptFile` was never called in any commit. It fetched `${nodeUrl}/file/${id}/${chunk}?token=` in a loop — the node's unauthenticated HTTP file API, which is finding C1 and was deleted in Phase 11.5. A client for an endpoint that no longer exists, kept alive by being exported. `decryptChunkBin` — every file download and every video segment — is untouched. `packages/meshbay-client/ui/` was resynchronised with `npm run sync-ui`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AsoWC3GmhNdwVFomW3QjH3
Diffstat (limited to 'packages')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/crypto.js73
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/file-utils.js34
2 files changed, 29 insertions, 78 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/crypto.js b/packages/meshbay-hub/src/meshbay_hub/static/crypto.js
index 1ddaa50..69b8c7a 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/crypto.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/crypto.js
@@ -8,7 +8,7 @@
*
* Usage:
* const gek = await importGEK(gekB64);
- * const plaintext = await decryptChunk(gek, fileHashHex, chunkIndex, nonceB64, ctB64);
+ * const plaintext = await decryptChunkBin(gek, fileHashHex, chunkIndex, nonce, ct);
*/
const CIPHER_INFO_PREFIX = new TextEncoder().encode('file:');
@@ -63,65 +63,16 @@ async function deriveChunkKey(gek, fileHashHex, chunkIndex) {
// ── Decryption ────────────────────────────────────────────────────────────────
-/**
- * Decrypt one chunk of a private group file.
- * @param {CryptoKey} gek - from importGEK()
- * @param {string} fileHashHex
- * @param {number} chunkIndex
- * @param {string} nonceB64 - 12-byte nonce, base64
- * @param {string} ctB64 - ciphertext + GCM tag, base64
- * @returns {Promise<Uint8Array>} plaintext
- */
-async function decryptChunk(gek, fileHashHex, chunkIndex, nonceB64, ctB64) {
- const chunkKey = await deriveChunkKey(gek, fileHashHex, chunkIndex);
- const nonce = b64decode(nonceB64);
- const ct = b64decode(ctB64);
- const plaintext = await crypto.subtle.decrypt(
- { name: 'AES-GCM', iv: nonce },
- chunkKey,
- ct,
- );
- return new Uint8Array(plaintext);
-}
-
-/**
- * Decrypt a full file by fetching and decrypting all chunks in order.
- * @param {CryptoKey} gek
- * @param {string} nodeUrl - base URL of node HTTP API
- * @param {string} fileId - blake3 hex hash (= file_id in index)
- * @param {string} fileHashHex - same as fileId (blake3 of file content)
- * @param {string} jwtToken
- * @returns {Promise<Blob>} decrypted file as Blob
- */
-async function decryptFile(gek, nodeUrl, fileId, fileHashHex, jwtToken) {
- const chunks = [];
- let chunkIdx = 0;
-
- while (true) {
- const sep = nodeUrl.includes('?') ? '&' : '?';
- const url = `${nodeUrl}/file/${fileId}/${chunkIdx}${sep}token=${jwtToken}`;
- const resp = await fetch(url);
- if (!resp.ok) break;
-
- const data = await resp.json();
- if (data.encrypted === false) {
- // Public group: data_b64 is plaintext
- chunks.push(b64decode(data.data_b64));
- } else {
- // Private group with AES-256-GCM
- const plain = await decryptChunk(
- gek, fileHashHex, chunkIdx,
- data.nonce_b64, data.ct_b64,
- );
- chunks.push(plain);
- }
-
- if (data.plaintext_size < 1024 * 1024) break; // last chunk (< 1MB)
- chunkIdx++;
- }
-
- return new Blob(chunks);
-}
+// `decryptChunk` (base64) and `decryptFile` were removed on 2026-09-03.
+//
+// `decryptChunk` was the real path until Phase 9.15 moved chunks to a binary wire
+// format; `decryptChunkBin` below replaced it, and MNP 0.15 removed the last node
+// that could still emit the base64 shape (the QUIC encoder, left behind by 9.15).
+//
+// `decryptFile` was never called by anything, in any commit: it fetched
+// `${nodeUrl}/file/${id}/${chunk}?token=` in a loop — the node's unauthenticated
+// HTTP file API, which is finding C1 and was deleted in Phase 11.5. A client for an
+// endpoint that no longer exists, kept alive only by being exported.
// ── Helpers ───────────────────────────────────────────────────────────────────
@@ -420,7 +371,7 @@ async function verifyNodeSignature(nodePkB64, sigB64, transcript) {
// Export for use in app.js
window.MeshBayCrypto = {
- importGEK, deriveChunkKey, decryptChunk, decryptChunkBin, decryptFile,
+ importGEK, deriveChunkKey, decryptChunkBin,
generateGEK, wrapGEK, unwrapGEK, encryptChunk, b64encode, b64decode,
adminTranscript, handshakeTranscript, handshakeProof, webrtcBinding,
joinTranscript, verifyNodeSignature, constantTimeEqual,
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js b/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js
index ba76ac9..09ee6c9 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js
@@ -110,13 +110,6 @@ function _saveBlob(blob, filename) {
URL.revokeObjectURL(url);
}
-function _b64ToU8(b64) {
- const bin = atob(b64);
- const arr = new Uint8Array(bin.length);
- for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
- return arr;
-}
-
// A dead transport (screen-lock WebRTC failure, see transport.js's
// _reconnectLoop) surfaces here as a rejected fetchChunk — TransportLostError
// when the pending request was killed outright, a plain timeout if it was
@@ -173,16 +166,23 @@ async function pipelinedDownload(transport, gekKey, fileId, totalChunks, onChunk
throw err;
}
const chunkMsg = await inflight[nextRecv];
- let plaintext;
- if (gekKey && chunkMsg.ct) {
- plaintext = await window.MeshBayCrypto.decryptChunkBin(
- gekKey, fileId, nextRecv, chunkMsg.nonce, chunkMsg.ct);
- } else if (gekKey && chunkMsg.ct_b64) {
- plaintext = await window.MeshBayCrypto.decryptChunk(
- gekKey, fileId, nextRecv, chunkMsg.nonce_b64, chunkMsg.ct_b64);
- } else {
- plaintext = _b64ToU8(chunkMsg.ct_b64 || chunkMsg.data_b64);
+ // One shape, and a refusal for anything else. There used to be two fallbacks
+ // below this: a base64 `ct_b64` chunk, which was the real wire format until
+ // the binary switch in Phase 9.15 and which no node has sent since, and a
+ // plaintext branch that base64-decoded `undefined` when neither field was
+ // there. That last one is why this now throws: a chunk we cannot decrypt has
+ // to stop the download, not write whatever it decoded into the file the user
+ // is saving. MNP 0.15 removed the last producer of the base64 shape (it was
+ // still emitted on the QUIC transport), so a node that sends one is a node
+ // older than the SPA serving this page.
+ if (!gekKey || !chunkMsg.ct) {
+ throw new Error(
+ `Chunk ${nextRecv} of ${fileId.slice(0, 8)} cannot be decrypted `
+ + `(${gekKey ? 'unexpected chunk format' : 'no group key'}) — `
+ + 'the node may be running an older version.');
}
+ const plaintext = await window.MeshBayCrypto.decryptChunkBin(
+ gekKey, fileId, nextRecv, chunkMsg.nonce, chunkMsg.ct);
if (writable) {
await writable.write(plaintext);
} else {
@@ -328,6 +328,6 @@ async function downloadDirectory(transfers, transport, gek, entries, dir, { setE
export {
FILE_ICONS,
formatSize, formatDate, PREVIEWABLE_TEXT, canPreview, CHUNK_SIZE,
- _openDownloadTarget, _saveBlob, _b64ToU8, pipelinedDownload, downloadEntry,
+ _openDownloadTarget, _saveBlob, pipelinedDownload, downloadEntry,
downloadDirectory,
};