| 1 | // Cross-implementation gate for the message payload — the encrypted envelope |
| 2 | // chat.js/shell.js build ({text, attachments, replyTo} stamped with name/uid/ts). |
| 3 | // The native client decrypts the same blobs (LChatCore's GroupKey + |
| 4 | // MessagePayload); if crypto.js's format or the payload's field names ever |
| 5 | // change, the two silently drift and native renders garbage. Pinned two ways: |
| 6 | // |
| 7 | // 1. a fresh encrypt → decrypt round-trips in JS (crypto.js self-consistency), and |
| 8 | // 2. the committed fixture (LChatCore's Tests/…/Fixtures/message-payload-vector.json) |
| 9 | // still decrypts to the expected payload — the SAME fixture the Swift test |
| 10 | // (`swift test` in ios/LChatCore, MessagePayloadVectorTests) consumes. |
| 11 | // |
| 12 | // If (2) fails, the format changed incompatibly: regenerate with --emit and |
| 13 | // update LChatCore (MessagePayload / Crypto.swift) to match in the same change. |
| 14 | import { readFileSync, writeFileSync } from "node:fs"; |
| 15 | import { fileURLToPath } from "node:url"; |
| 16 | import { dirname, join } from "node:path"; |
| 17 | import { generateKey, importKey, encrypt, decrypt } from "../web/static/crypto.js"; |
| 18 | |
| 19 | const here = dirname(fileURLToPath(import.meta.url)); |
| 20 | const fixturePath = join( |
| 21 | here, "..", "ios", "LChatCore", "Tests", "LChatCoreTests", "Fixtures", "message-payload-vector.json" |
| 22 | ); |
| 23 | |
| 24 | // The full envelope shape, every optional field populated — so the Swift side |
| 25 | // proves it decodes replies, attachment dimensions, ts and uid, not just text. |
| 26 | const payload = { |
| 27 | text: "Hello from the vector! 🎉", |
| 28 | name: "Paul", |
| 29 | uid: "u_vector", |
| 30 | ts: 1751830000000, |
| 31 | attachments: [ |
| 32 | { id: "blob123", name: "photo.jpg", type: "image/jpeg", w: 800, h: 600, size: 123456 }, |
| 33 | { id: "blob456", name: "clip.mp4", type: "video/mp4", w: 1280, h: 720, size: 9876543, poster: "blob457", dur: 12.5 }, |
| 34 | ], |
| 35 | replyTo: { id: 41, name: "Ada", snippet: "the original line" }, |
| 36 | }; |
| 37 | |
| 38 | function assert(cond, msg) { |
| 39 | if (!cond) { |
| 40 | console.error("FAIL:", msg); |
| 41 | process.exit(1); |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | // `node test/messageblob.test.mjs --emit` regenerates the committed fixture |
| 46 | // (encryption picks a fresh IV, so the fixture must be frozen, not recomputed). |
| 47 | async function emit() { |
| 48 | const { key, b64 } = await generateKey(); |
| 49 | const blob = await encrypt(key, payload); |
| 50 | const fixture = { |
| 51 | _comment: |
| 52 | "Message-payload test vector. Generated by test/messageblob.test.mjs from " + |
| 53 | "web/static/crypto.js. LChatCore's MessagePayloadVectorTests decrypts `blob` " + |
| 54 | "with `keyB64` and must decode every `expect` field — a cross-implementation " + |
| 55 | "gate so native message decryption never drifts from the web's. " + |
| 56 | "Regenerate with: node test/messageblob.test.mjs --emit", |
| 57 | keyB64: b64, |
| 58 | blob, |
| 59 | expect: payload, |
| 60 | }; |
| 61 | writeFileSync(fixturePath, JSON.stringify(fixture, null, 2) + "\n"); |
| 62 | console.log("wrote", fixturePath); |
| 63 | } |
| 64 | |
| 65 | async function test() { |
| 66 | // (1) crypto.js self-consistency on this payload shape. |
| 67 | const { key } = await generateKey(); |
| 68 | const round = await decrypt(key, await encrypt(key, payload)); |
| 69 | assert(JSON.stringify(round) === JSON.stringify(payload), "fresh encrypt/decrypt round-trips"); |
| 70 | |
| 71 | // (2) the committed fixture (shared with the Swift test) still decrypts. |
| 72 | const fx = JSON.parse(readFileSync(fixturePath, "utf8")); |
| 73 | const got = await decrypt(await importKey(fx.keyB64), fx.blob); |
| 74 | assert(JSON.stringify(got) === JSON.stringify(fx.expect), "committed fixture decrypts to expected payload"); |
| 75 | |
| 76 | console.log("messageblob.test.mjs OK"); |
| 77 | } |
| 78 | |
| 79 | if (process.argv.includes("--emit")) await emit(); |
| 80 | else await test(); |
| 81 | |