diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-03 15:20:52 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-03 15:20:52 +0200 |
| commit | 0b3ffdb8d7f07aee817ece24fd44a24ac60e6714 (patch) | |
| tree | 4f65c9bb9128abf18f7739a711c77cfc8012fc06 /packages | |
| parent | c1be7571973c3d0b671ed4db2da41266ae3099d8 (diff) | |
| download | meshbay-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.js | 73 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/file-utils.js | 34 |
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, }; |