mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-06-06 22:01:54 +00:00
xftp-web: remove debug logs (#1798)
Co-authored-by: Paul Bottinelli <paul.bottinelli@trailofbits.com>
This commit is contained in:
@@ -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 });
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
+18
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user