| 1 | // Cross-implementation gate for the BINARY crypto path — encryptBytes / |
| 2 | // decryptBytes, the raw `iv||ct||tag` bytes attachments upload and download as |
| 3 | // (no base64, no JSON). The native client opens the same blobs (LChatCore's |
| 4 | // GroupKey.decryptBytes, via BlobImageLoader); drift here means photos decrypt |
| 5 | // on one platform and not the other. Pinned two ways: |
| 6 | // |
| 7 | // 1. a fresh encryptBytes → decryptBytes round-trips in JS, and |
| 8 | // 2. the committed fixture (LChatCore's Tests/…/Fixtures/attachment-bytes-vector.json) |
| 9 | // still decrypts to the expected bytes — the SAME fixture the Swift test |
| 10 | // (`swift test` in ios/LChatCore, AttachmentBytesVectorTests) consumes. |
| 11 | // |
| 12 | // If (2) fails, regenerate with --emit and update LChatCore in the same change. |
| 13 | import { readFileSync, writeFileSync } from "node:fs"; |
| 14 | import { fileURLToPath } from "node:url"; |
| 15 | import { dirname, join } from "node:path"; |
| 16 | import { generateKey, importKey, encryptBytes, decryptBytes, bytesToB64url, b64urlToBytes } from "../web/static/crypto.js"; |
| 17 | |
| 18 | const here = dirname(fileURLToPath(import.meta.url)); |
| 19 | const fixturePath = join( |
| 20 | here, "..", "ios", "LChatCore", "Tests", "LChatCoreTests", "Fixtures", "attachment-bytes-vector.json" |
| 21 | ); |
| 22 | |
| 23 | // Every byte value once — catches any encoding/sign slip in either language. |
| 24 | function knownBytes() { |
| 25 | const raw = new Uint8Array(256); |
| 26 | for (let i = 0; i < 256; i++) raw[i] = i; |
| 27 | return raw; |
| 28 | } |
| 29 | |
| 30 | function assert(cond, msg) { |
| 31 | if (!cond) { |
| 32 | console.error("FAIL:", msg); |
| 33 | process.exit(1); |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | // `node test/blobbytes.test.mjs --emit` regenerates the committed fixture (the |
| 38 | // encrypt uses a fresh IV, so the fixture must be frozen, not recomputed). |
| 39 | async function emit() { |
| 40 | const { key, b64 } = await generateKey(); |
| 41 | const blob = await encryptBytes(key, knownBytes()); |
| 42 | const fixture = { |
| 43 | _comment: |
| 44 | "Attachment-bytes test vector (binary iv||ct||tag path). Generated by " + |
| 45 | "test/blobbytes.test.mjs from web/static/crypto.js. LChatCore's " + |
| 46 | "AttachmentBytesVectorTests opens blobB64url's raw bytes with keyB64 via " + |
| 47 | "GroupKey.decryptBytes and must get plaintextB64url's bytes back — the " + |
| 48 | "gate for native attachment/avatar decryption. " + |
| 49 | "Regenerate with: node test/blobbytes.test.mjs --emit", |
| 50 | keyB64: b64, |
| 51 | blobB64url: bytesToB64url(blob), |
| 52 | plaintextB64url: bytesToB64url(knownBytes()), |
| 53 | }; |
| 54 | writeFileSync(fixturePath, JSON.stringify(fixture, null, 2) + "\n"); |
| 55 | console.log("wrote", fixturePath); |
| 56 | } |
| 57 | |
| 58 | async function test() { |
| 59 | // (1) crypto.js self-consistency on the binary path. |
| 60 | const { key } = await generateKey(); |
| 61 | const round = await decryptBytes(key, await encryptBytes(key, knownBytes())); |
| 62 | assert(bytesToB64url(round) === bytesToB64url(knownBytes()), "fresh encryptBytes/decryptBytes round-trips"); |
| 63 | |
| 64 | // (2) the committed fixture (shared with the Swift test) still decrypts. |
| 65 | const fx = JSON.parse(readFileSync(fixturePath, "utf8")); |
| 66 | const got = await decryptBytes(await importKey(fx.keyB64), b64urlToBytes(fx.blobB64url)); |
| 67 | assert(bytesToB64url(got) === fx.plaintextB64url, "committed fixture decrypts to expected bytes"); |
| 68 | |
| 69 | console.log("blobbytes.test.mjs OK"); |
| 70 | } |
| 71 | |
| 72 | if (process.argv.includes("--emit")) await emit(); |
| 73 | else await test(); |
| 74 | |