diff --git a/apps/xftp-server/static/xftp-web-bundle/crypto.worker.js b/apps/xftp-server/static/xftp-web-bundle/crypto.worker.js index 20b6859ac..915041099 100644 --- a/apps/xftp-server/static/xftp-web-bundle/crypto.worker.js +++ b/apps/xftp-server/static/xftp-web-bundle/crypto.worker.js @@ -619,7 +619,6 @@ async function handleEncrypt(id, data, fileName) { const digest = sha512Streaming([encData], (done) => { self.postMessage({ id, type: "progress", done: source.length + done, total }); }, encDataLen); - console.log(`[WORKER-DBG] encrypt: encData.len=${encData.length} digest=${_whex(digest, 64)} chunkSizes=[${chunkSizes.join(",")}]`); const dir = await getSessionDir(); const fileHandle = await dir.getFileHandle("upload.bin", { create: true }); const writeHandle = await fileHandle.createSyncAccessHandle(); @@ -643,9 +642,7 @@ function handleReadChunk(id, offset, size) { } async function handleDecryptAndStore(id, dhSecret, nonce, body, chunkDigest, chunkNo) { const bodyArr = new Uint8Array(body); - console.log(`[WORKER-DBG] store chunk=${chunkNo} body.len=${bodyArr.length} nonce=${_whex(nonce, 24)} dhSecret=${_whex(dhSecret)} digest=${_whex(chunkDigest, 32)} body[0..8]=${_whex(bodyArr)} body[-8..]=${_whex(bodyArr.slice(-8))}`); const decrypted = decryptReceivedChunk(dhSecret, nonce, bodyArr, chunkDigest); - console.log(`[WORKER-DBG] decrypted chunk=${chunkNo} len=${decrypted.length} [0..8]=${_whex(decrypted)} [-8..]=${_whex(decrypted.slice(-8))}`); if (useMemory) { memoryChunks.set(chunkNo, decrypted); self.postMessage({ id, type: "stored" }); @@ -660,7 +657,6 @@ async function handleDecryptAndStore(id, dhSecret, nonce, body, chunkDigest, chu currentDownloadOffset += decrypted.length; chunkMeta.set(chunkNo, { offset, size: decrypted.length }); const written = downloadWriteHandle.write(decrypted, { at: offset }); - console.log(`[WORKER-DBG] OPFS write chunk=${chunkNo} offset=${offset} size=${decrypted.length} written=${written}`); if (written !== decrypted.length) { console.warn(`[WORKER] OPFS write failed chunk=${chunkNo}: ${written}/${decrypted.length}, falling back to in-memory storage`); for (const [cn, meta] of chunkMeta.entries()) { @@ -684,23 +680,16 @@ async function handleDecryptAndStore(id, dhSecret, nonce, body, chunkDigest, chu return; } downloadWriteHandle.flush(); - const verifyBuf = new Uint8Array(Math.min(8, decrypted.length)); - downloadWriteHandle.read(verifyBuf, { at: offset }); - const verifyEnd = new Uint8Array(Math.min(8, decrypted.length)); - downloadWriteHandle.read(verifyEnd, { at: offset + decrypted.length - verifyEnd.length }); - console.log(`[WORKER-DBG] OPFS verify chunk=${chunkNo} readBack[0..8]=${_whex(verifyBuf)} readBack[-8..]=${_whex(verifyEnd)} expected[0..8]=${_whex(decrypted)} expected[-8..]=${_whex(decrypted.slice(-8))}`); self.postMessage({ id, type: "stored" }); } async function handleVerifyAndDecrypt(id, size, digest, key, nonce) { - console.log(`[WORKER-DBG] verify: expectedSize=${size} expectedDigest=${_whex(digest, 64)} useMemory=${useMemory} chunkMeta.size=${chunkMeta.size} memoryChunks.size=${memoryChunks.size}`); const chunks = []; let totalSize = 0; const total = size * 3; let done = 0; if (useMemory) { const sorted = [...memoryChunks.entries()].sort((a, b) => a[0] - b[0]); - for (const [chunkNo, data] of sorted) { - console.log(`[WORKER-DBG] verify memory chunk=${chunkNo} size=${data.length}`); + for (const [, data] of sorted) { chunks.push(data); totalSize += data.length; done += data.length; @@ -715,12 +704,10 @@ async function handleVerifyAndDecrypt(id, size, digest, key, nonce) { const dir = await getSessionDir(); const fileHandle = await dir.getFileHandle("download.bin"); const readHandle = await fileHandle.createSyncAccessHandle(); - console.log(`[WORKER-DBG] verify: OPFS file size=${readHandle.getSize()}`); const sortedEntries = [...chunkMeta.entries()].sort((a, b) => a[0] - b[0]); - for (const [chunkNo, meta] of sortedEntries) { + for (const [, meta] of sortedEntries) { const buf = new Uint8Array(meta.size); - const bytesRead = readHandle.read(buf, { at: meta.offset }); - console.log(`[WORKER-DBG] verify read chunk=${chunkNo} offset=${meta.offset} size=${meta.size} bytesRead=${bytesRead} [0..8]=${_whex(buf)} [-8..]=${_whex(buf.slice(-8))}`); + readHandle.read(buf, { at: meta.offset }); chunks.push(buf); totalSize += meta.size; done += meta.size; @@ -745,20 +732,9 @@ async function handleVerifyAndDecrypt(id, size, digest, key, nonce) { } const actualDigest = r.crypto_hash_sha512_final(state); if (!digestEqual(actualDigest, digest)) { - console.error(`[WORKER-DBG] DIGEST MISMATCH: expected=${_whex(digest, 64)} actual=${_whex(actualDigest, 64)} chunks=${chunks.length} totalSize=${totalSize}`); - const state2 = r.crypto_hash_sha512_init(); - for (let i = 0; i < chunks.length; i++) { - const chunk = chunks[i]; - for (let off = 0; off < chunk.length; off += hashSEG) { - r.crypto_hash_sha512_update(state2, chunk.subarray(off, Math.min(off + hashSEG, chunk.length))); - } - const chunkDigest = sha512Streaming([chunk]); - console.error(`[WORKER-DBG] chunk[${i}] size=${chunk.length} sha512=${_whex(chunkDigest, 32)}… [0..8]=${_whex(chunk)} [-8..]=${_whex(chunk.slice(-8))}`); - } self.postMessage({ id, type: "error", message: "File digest mismatch" }); return; } - console.log(`[WORKER-DBG] verify: digest OK`); const result = decryptChunks(BigInt(size), chunks, key, nonce, (d) => { self.postMessage({ id, type: "progress", done: size * 2 + d, total }); }); diff --git a/apps/xftp-server/static/xftp-web-bundle/index.js b/apps/xftp-server/static/xftp-web-bundle/index.js index 1d41088c8..49b8d9d7f 100644 --- a/apps/xftp-server/static/xftp-web-bundle/index.js +++ b/apps/xftp-server/static/xftp-web-bundle/index.js @@ -334,11 +334,6 @@ class WorkerBackend { const nonceCopy = new Uint8Array(nonce); const digestCopy = new Uint8Array(digest); const buf = this.toTransferable(body); - const hex = (b, n = 8) => { - const u = b instanceof ArrayBuffer ? new Uint8Array(b) : b; - return Array.from(u.slice(0, n)).map((x) => x.toString(16).padStart(2, "0")).join(""); - }; - console.log(`[BACKEND-DBG] chunk=${chunkNo} body.len=${body.length} body.byteOff=${body.byteOffset} buf.byteLen=${buf.byteLength} nonce=${hex(nonceCopy, 24)} dhSecret=${hex(dhSecretCopy)} digest=${hex(digestCopy, 32)} buf[0..8]=${hex(buf)} body[-8..]=${hex(body.slice(-8))}`); await this.send( { type: "decryptAndStoreChunk", dhSecret: dhSecretCopy, nonce: nonceCopy, body: buf, chunkDigest: digestCopy, chunkNo }, [buf] @@ -10913,14 +10908,12 @@ async function sendXFTPCommandOnce(client, privateKey, entityId, cmdBytes, chunk const block = encodeAuthTransmission(client.sessionId, corrId, entityId, cmdBytes, privateKey); const reqBody = chunkData ? concatBytes$1(block, chunkData) : block; const fullResp = await client.transport.post(reqBody); - console.log(`[XFTP-DBG] sendOnce: fullResp.length=${fullResp.length} entityId=${_hex(entityId)} cmdTag=${cmdBytes[0]}`); if (fullResp.length < XFTP_BLOCK_SIZE) { console.error("[XFTP] Response too short: %d bytes (expected >= %d)", fullResp.length, XFTP_BLOCK_SIZE); throw new Error("Server response too short"); } const respBlock = fullResp.subarray(0, XFTP_BLOCK_SIZE); const body = fullResp.subarray(XFTP_BLOCK_SIZE); - console.log(`[XFTP-DBG] sendOnce: body.length=${body.length} body.byteOffset=${body.byteOffset} body.buffer.byteLength=${body.buffer.byteLength}`); const raw = blockUnpad(respBlock); if (raw.length < 20) { const text = new TextDecoder().decode(raw); @@ -10939,18 +10932,13 @@ async function sendXFTPCommandOnce(client, privateKey, entityId, cmdBytes, chunk } return { response, body }; } -function _hex(b, n = 8) { - return Array.from(b.slice(0, n)).map((x) => x.toString(16).padStart(2, "0")).join(""); -} async function sendXFTPCommand(agent, server, privateKey, entityId, cmdBytes, chunkData, maxRetries = 3) { let clientP = getXFTPServerClient(agent, server); let client = await clientP; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { - if (attempt > 1) console.log(`[XFTP-DBG] sendCmd: retry attempt=${attempt}/${maxRetries}`); return await sendXFTPCommandOnce(client, privateKey, entityId, cmdBytes, chunkData); } catch (e) { - console.log(`[XFTP-DBG] sendCmd: attempt=${attempt} failed: ${e instanceof Error ? e.message : String(e)} retriable=${isRetriable(e)}`); if (!isRetriable(e)) { throw categorizeError(e); } @@ -10979,7 +10967,6 @@ async function downloadXFTPChunkRaw(agent, server, rpKey, fId) { const { response, body } = await sendXFTPCommand(agent, server, rpKey, fId, cmd); if (response.type !== "FRFile") throw new Error("unexpected response: " + response.type); const dhSecret = dh(response.rcvDhKey, privateKey); - console.log(`[XFTP-DBG] dlChunkRaw: body.length=${body.length} nonce=${_hex(response.nonce, 24)} dhSecret=${_hex(dhSecret)} body[0..8]=${_hex(body)} body[-8..]=${_hex(body.slice(-8))}`); return { dhSecret, nonce: response.nonce, body }; } async function downloadXFTPChunk(agent, server, rpKey, fId, digest) { @@ -11012,7 +10999,6 @@ function encryptFileForUpload(source, fileName) { const encSize = BigInt(chunkSizes.reduce((a, b) => a + b, 0)); const encData = encryptFile(source, fileHdr, key, nonce, fileSize, encSize); const digest = sha512Streaming([encData]); - console.log(`[AGENT-DBG] encrypt: encData.len=${encData.length} digest=${_dbgHex(digest, 64)} chunkSizes=[${chunkSizes.join(",")}]`); return { encData, digest, key, nonce, chunkSizes }; } const DEFAULT_REDIRECT_THRESHOLD = 400; @@ -11169,9 +11155,7 @@ async function downloadFileRaw(agent, fd, onRawChunk, options) { if (err) throw new Error("downloadFileRaw: " + err); const { onProgress} = options ?? {}; if (fd.redirect !== null) { - console.log(`[AGENT-DBG] resolving redirect: outer size=${fd.size} chunks=${fd.chunks.length}`); fd = await resolveRedirect(agent, fd); - console.log(`[AGENT-DBG] resolved: size=${fd.size} chunks=${fd.chunks.length} digest=${Array.from(fd.digest.slice(0, 16)).map((x) => x.toString(16).padStart(2, "0")).join("")}…`); } const resolvedFd = fd; let downloaded = 0; @@ -11189,7 +11173,6 @@ async function downloadFileRaw(agent, fd, onRawChunk, options) { const seed = decodePrivKeyEd25519(replica.replicaKey); const kp = ed25519KeyPairFromSeed(seed); const raw = await downloadXFTPChunkRaw(agent, server, kp.privateKey, replica.replicaId); - console.log(`[AGENT-DBG] chunk=${chunk.chunkNo} body.len=${raw.body.length} expectedChunkSize=${chunk.chunkSize} digest=${_dbgHex(chunk.digest, 32)} body.byteOffset=${raw.body.byteOffset} body.buffer.byteLength=${raw.body.buffer.byteLength}`); await onRawChunk({ chunkNo: chunk.chunkNo, dhSecret: raw.dhSecret, diff --git a/tests/XFTPWebTests.hs b/tests/XFTPWebTests.hs index c9a98eef1..0172a6dc7 100644 --- a/tests/XFTPWebTests.hs +++ b/tests/XFTPWebTests.hs @@ -14,7 +14,7 @@ module XFTPWebTests (xftpWebTests) where import Control.Concurrent (forkIO, newEmptyMVar, putMVar, takeMVar) -import Control.Monad (replicateM, when) +import Control.Monad (forM_, replicateM, when) import Crypto.Error (throwCryptoError) import qualified Crypto.PubKey.Curve25519 as X25519 import qualified Crypto.PubKey.Ed25519 as Ed25519 @@ -170,6 +170,7 @@ jsOut expr = "process.stdout.write(Buffer.from(" <> expr <> "));" xftpWebTests :: IO () -> Spec xftpWebTests dbCleanup = do + xftpWebSourceHygieneTests distExists <- runIO $ doesDirectoryExist (xftpWebDir <> "/dist") if distExists then do @@ -193,6 +194,22 @@ xftpWebTests dbCleanup = do it "skipped (run 'cd xftp-web && npm install && npm run build' first)" $ pendingWith "TS project not compiled" +xftpWebSourceHygieneTests :: Spec +xftpWebSourceHygieneTests = describe "source hygiene" $ + it "does not commit XFTP web debug logs that expose secrets or plaintext" $ do + let files = + [ "xftp-web/src/client.ts", + "xftp-web/src/agent.ts", + "xftp-web/web/crypto-backend.ts", + "xftp-web/web/crypto.worker.ts", + "apps/xftp-server/static/xftp-web-bundle/index.js", + "apps/xftp-server/static/xftp-web-bundle/crypto.worker.js" + ] + markers = ["XFTP-DBG", "AGENT-DBG", "BACKEND-DBG", "WORKER-DBG"] + forM_ files $ \path -> do + contents <- B.readFile path + mapM_ (\marker -> contents `shouldNotSatisfy` B.isInfixOf marker) markers + -- ── protocol/encoding ────────────────────────────────────────────── tsEncodingTests :: Spec diff --git a/xftp-web/src/agent.ts b/xftp-web/src/agent.ts index c8c892234..4a3b2fcff 100644 --- a/xftp-web/src/agent.ts +++ b/xftp-web/src/agent.ts @@ -90,7 +90,6 @@ export function encryptFileForUpload(source: Uint8Array, fileName: string): Encr const encSize = BigInt(chunkSizes.reduce((a, b) => a + b, 0)) const encData = encryptFile(source, fileHdr, key, nonce, fileSize, encSize) const digest = sha512Streaming([encData]) - console.log(`[AGENT-DBG] encrypt: encData.len=${encData.length} digest=${_dbgHex(digest, 64)} chunkSizes=[${chunkSizes.join(',')}]`) return {encData, digest, key, nonce, chunkSizes} } @@ -280,9 +279,7 @@ export async function downloadFileRaw( const {onProgress, concurrency = 1} = options ?? {} // Resolve redirect on main thread (redirect data is small) if (fd.redirect !== null) { - console.log(`[AGENT-DBG] resolving redirect: outer size=${fd.size} chunks=${fd.chunks.length}`) fd = await resolveRedirect(agent, fd) - console.log(`[AGENT-DBG] resolved: size=${fd.size} chunks=${fd.chunks.length} digest=${Array.from(fd.digest.slice(0, 16)).map(x => x.toString(16).padStart(2, '0')).join('')}…`) } const resolvedFd = fd // Group chunks by server, sequential within each server, parallel across servers @@ -301,7 +298,6 @@ export async function downloadFileRaw( const seed = decodePrivKeyEd25519(replica.replicaKey) const kp = ed25519KeyPairFromSeed(seed) const raw = await downloadXFTPChunkRaw(agent, server, kp.privateKey, replica.replicaId) - console.log(`[AGENT-DBG] chunk=${chunk.chunkNo} body.len=${raw.body.length} expectedChunkSize=${chunk.chunkSize} digest=${_dbgHex(chunk.digest, 32)} body.byteOffset=${raw.body.byteOffset} body.buffer.byteLength=${raw.body.buffer.byteLength}`) await onRawChunk({ chunkNo: chunk.chunkNo, dhSecret: raw.dhSecret, @@ -377,10 +373,6 @@ export async function deleteFile(agent: XFTPClientAgent, sndDescription: FileDes // -- Internal -function _dbgHex(b: Uint8Array, n = 8): string { - return Array.from(b.slice(0, n)).map(x => x.toString(16).padStart(2, '0')).join('') -} - function digestEqual(a: Uint8Array, b: Uint8Array): boolean { if (a.length !== b.length) return false let diff = 0 diff --git a/xftp-web/src/client.ts b/xftp-web/src/client.ts index 52fce11e8..0c6cd4d06 100644 --- a/xftp-web/src/client.ts +++ b/xftp-web/src/client.ts @@ -311,14 +311,12 @@ async function sendXFTPCommandOnce( const block = encodeAuthTransmission(client.sessionId, corrId, entityId, cmdBytes, privateKey) const reqBody = chunkData ? concatBytes(block, chunkData) : block const fullResp = await client.transport.post(reqBody) - console.log(`[XFTP-DBG] sendOnce: fullResp.length=${fullResp.length} entityId=${_hex(entityId)} cmdTag=${cmdBytes[0]}`) if (fullResp.length < XFTP_BLOCK_SIZE) { console.error('[XFTP] Response too short: %d bytes (expected >= %d)', fullResp.length, XFTP_BLOCK_SIZE) throw new Error("Server response too short") } const respBlock = fullResp.subarray(0, XFTP_BLOCK_SIZE) const body = fullResp.subarray(XFTP_BLOCK_SIZE) - console.log(`[XFTP-DBG] sendOnce: body.length=${body.length} body.byteOffset=${body.byteOffset} body.buffer.byteLength=${body.buffer.byteLength}`) // Detect padded error strings (HANDSHAKE, SESSION) before decodeTransmission const raw = blockUnpad(respBlock) if (raw.length < 20) { @@ -339,10 +337,6 @@ async function sendXFTPCommandOnce( return {response, body} } -function _hex(b: Uint8Array, n = 8): string { - return Array.from(b.slice(0, n)).map(x => x.toString(16).padStart(2, '0')).join('') -} - // -- Send command (with retry + reconnect) export async function sendXFTPCommand( @@ -358,10 +352,8 @@ export async function sendXFTPCommand( let client = await clientP for (let attempt = 1; attempt <= maxRetries; attempt++) { try { - if (attempt > 1) console.log(`[XFTP-DBG] sendCmd: retry attempt=${attempt}/${maxRetries}`) return await sendXFTPCommandOnce(client, privateKey, entityId, cmdBytes, chunkData) } catch (e) { - console.log(`[XFTP-DBG] sendCmd: attempt=${attempt} failed: ${e instanceof Error ? e.message : String(e)} retriable=${isRetriable(e)}`) if (!isRetriable(e)) { throw categorizeError(e) } @@ -416,7 +408,6 @@ export async function downloadXFTPChunkRaw( const {response, body} = await sendXFTPCommand(agent, server, rpKey, fId, cmd) if (response.type !== "FRFile") throw new Error("unexpected response: " + response.type) const dhSecret = dh(response.rcvDhKey, privateKey) - console.log(`[XFTP-DBG] dlChunkRaw: body.length=${body.length} nonce=${_hex(response.nonce, 24)} dhSecret=${_hex(dhSecret)} body[0..8]=${_hex(body)} body[-8..]=${_hex(body.slice(-8))}`) return {dhSecret, nonce: response.nonce, body} } @@ -444,4 +435,3 @@ export async function pingXFTP(agent: XFTPClientAgent, server: XFTPServer): Prom const response = decodeResponse(command) if (response.type !== "FRPong") throw new Error("unexpected response: " + response.type) } - diff --git a/xftp-web/web/crypto-backend.ts b/xftp-web/web/crypto-backend.ts index 9319d6f99..15624121e 100644 --- a/xftp-web/web/crypto-backend.ts +++ b/xftp-web/web/crypto-backend.ts @@ -108,11 +108,6 @@ class WorkerBackend implements CryptoBackend { const nonceCopy = new Uint8Array(nonce) const digestCopy = new Uint8Array(digest) const buf = this.toTransferable(body) - const hex = (b: Uint8Array | ArrayBuffer, n = 8) => { - const u = b instanceof ArrayBuffer ? new Uint8Array(b) : b - return Array.from(u.slice(0, n)).map(x => x.toString(16).padStart(2, '0')).join('') - } - console.log(`[BACKEND-DBG] chunk=${chunkNo} body.len=${body.length} body.byteOff=${body.byteOffset} buf.byteLen=${buf.byteLength} nonce=${hex(nonceCopy, 24)} dhSecret=${hex(dhSecretCopy)} digest=${hex(digestCopy, 32)} buf[0..8]=${hex(buf)} body[-8..]=${hex(body.slice(-8))}`) await this.send( {type: 'decryptAndStoreChunk', dhSecret: dhSecretCopy, nonce: nonceCopy, body: buf, chunkDigest: digestCopy, chunkNo}, [buf] diff --git a/xftp-web/web/crypto.worker.ts b/xftp-web/web/crypto.worker.ts index a2bd84843..5c0549ef0 100644 --- a/xftp-web/web/crypto.worker.ts +++ b/xftp-web/web/crypto.worker.ts @@ -59,7 +59,6 @@ async function handleEncrypt(id: number, data: ArrayBuffer, fileName: string) { const digest = sha512Streaming([encData], (done) => { self.postMessage({id, type: 'progress', done: source.length + done, total}) }, encDataLen) - console.log(`[WORKER-DBG] encrypt: encData.len=${encData.length} digest=${_whex(digest, 64)} chunkSizes=[${chunkSizes.join(',')}]`) // Write to OPFS const dir = await getSessionDir() @@ -93,9 +92,7 @@ async function handleDecryptAndStore( body: ArrayBuffer, chunkDigest: Uint8Array, chunkNo: number ) { const bodyArr = new Uint8Array(body) - console.log(`[WORKER-DBG] store chunk=${chunkNo} body.len=${bodyArr.length} nonce=${_whex(nonce, 24)} dhSecret=${_whex(dhSecret)} digest=${_whex(chunkDigest, 32)} body[0..8]=${_whex(bodyArr)} body[-8..]=${_whex(bodyArr.slice(-8))}`) const decrypted = decryptReceivedChunk(dhSecret, nonce, bodyArr, chunkDigest) - console.log(`[WORKER-DBG] decrypted chunk=${chunkNo} len=${decrypted.length} [0..8]=${_whex(decrypted)} [-8..]=${_whex(decrypted.slice(-8))}`) if (useMemory) { memoryChunks.set(chunkNo, decrypted) @@ -113,7 +110,6 @@ async function handleDecryptAndStore( currentDownloadOffset += decrypted.length chunkMeta.set(chunkNo, {offset, size: decrypted.length}) const written = downloadWriteHandle.write(decrypted, {at: offset}) - console.log(`[WORKER-DBG] OPFS write chunk=${chunkNo} offset=${offset} size=${decrypted.length} written=${written}`) if (written !== decrypted.length) { console.warn(`[WORKER] OPFS write failed chunk=${chunkNo}: ${written}/${decrypted.length}, falling back to in-memory storage`) @@ -140,21 +136,12 @@ async function handleDecryptAndStore( downloadWriteHandle.flush() - // Verify: read back and compare first/last 8 bytes - const verifyBuf = new Uint8Array(Math.min(8, decrypted.length)) - downloadWriteHandle.read(verifyBuf, {at: offset}) - const verifyEnd = new Uint8Array(Math.min(8, decrypted.length)) - downloadWriteHandle.read(verifyEnd, {at: offset + decrypted.length - verifyEnd.length}) - console.log(`[WORKER-DBG] OPFS verify chunk=${chunkNo} readBack[0..8]=${_whex(verifyBuf)} readBack[-8..]=${_whex(verifyEnd)} expected[0..8]=${_whex(decrypted)} expected[-8..]=${_whex(decrypted.slice(-8))}`) - self.postMessage({id, type: 'stored'}) } async function handleVerifyAndDecrypt( id: number, size: number, digest: Uint8Array, key: Uint8Array, nonce: Uint8Array ) { - console.log(`[WORKER-DBG] verify: expectedSize=${size} expectedDigest=${_whex(digest, 64)} useMemory=${useMemory} chunkMeta.size=${chunkMeta.size} memoryChunks.size=${memoryChunks.size}`) - // Read chunks — from memory (fallback) or OPFS const chunks: Uint8Array[] = [] let totalSize = 0 @@ -162,8 +149,7 @@ async function handleVerifyAndDecrypt( let done = 0 if (useMemory) { const sorted = [...memoryChunks.entries()].sort((a, b) => a[0] - b[0]) - for (const [chunkNo, data] of sorted) { - console.log(`[WORKER-DBG] verify memory chunk=${chunkNo} size=${data.length}`) + for (const [, data] of sorted) { chunks.push(data) totalSize += data.length done += data.length @@ -179,12 +165,10 @@ async function handleVerifyAndDecrypt( const dir = await getSessionDir() const fileHandle = await dir.getFileHandle('download.bin') const readHandle = await fileHandle.createSyncAccessHandle() - console.log(`[WORKER-DBG] verify: OPFS file size=${readHandle.getSize()}`) const sortedEntries = [...chunkMeta.entries()].sort((a, b) => a[0] - b[0]) - for (const [chunkNo, meta] of sortedEntries) { + for (const [, meta] of sortedEntries) { const buf = new Uint8Array(meta.size) - const bytesRead = readHandle.read(buf, {at: meta.offset}) - console.log(`[WORKER-DBG] verify read chunk=${chunkNo} offset=${meta.offset} size=${meta.size} bytesRead=${bytesRead} [0..8]=${_whex(buf)} [-8..]=${_whex(buf.slice(-8))}`) + readHandle.read(buf, {at: meta.offset}) chunks.push(buf) totalSize += meta.size done += meta.size @@ -212,20 +196,9 @@ async function handleVerifyAndDecrypt( } const actualDigest = sodium.crypto_hash_sha512_final(state) if (!digestEqual(actualDigest, digest)) { - console.error(`[WORKER-DBG] DIGEST MISMATCH: expected=${_whex(digest, 64)} actual=${_whex(actualDigest, 64)} chunks=${chunks.length} totalSize=${totalSize}`) - const state2 = sodium.crypto_hash_sha512_init() as unknown as import('libsodium-wrappers').StateAddress - for (let i = 0; i < chunks.length; i++) { - const chunk = chunks[i] - for (let off = 0; off < chunk.length; off += hashSEG) { - sodium.crypto_hash_sha512_update(state2, chunk.subarray(off, Math.min(off + hashSEG, chunk.length))) - } - const chunkDigest = sha512Streaming([chunk]) - console.error(`[WORKER-DBG] chunk[${i}] size=${chunk.length} sha512=${_whex(chunkDigest, 32)}… [0..8]=${_whex(chunk)} [-8..]=${_whex(chunk.slice(-8))}`) - } self.postMessage({id, type: 'error', message: 'File digest mismatch'}) return } - console.log(`[WORKER-DBG] verify: digest OK`) // File-level decrypt with byte-level progress const result = decryptChunks(BigInt(size), chunks, key, nonce, (d) => { @@ -312,10 +285,6 @@ self.onmessage = (e: MessageEvent) => { // ── Helpers ───────────────────────────────────────────────────── -function _whex(b: Uint8Array, n = 8): string { - return Array.from(b.slice(0, n)).map(x => x.toString(16).padStart(2, '0')).join('') -} - function digestEqual(a: Uint8Array, b: Uint8Array): boolean { if (a.length !== b.length) return false let diff = 0