// Cross-implementation gate for the message payload — the encrypted envelope // chat.js/shell.js build ({text, attachments, replyTo} stamped with name/uid/ts). // The native client decrypts the same blobs (LChatCore's GroupKey + // MessagePayload); if crypto.js's format or the payload's field names ever // change, the two silently drift and native renders garbage. Pinned two ways: // // 1. a fresh encrypt → decrypt round-trips in JS (crypto.js self-consistency), and // 2. the committed fixture (LChatCore's Tests/…/Fixtures/message-payload-vector.json) // still decrypts to the expected payload — the SAME fixture the Swift test // (`swift test` in ios/LChatCore, MessagePayloadVectorTests) consumes. // // If (2) fails, the format changed incompatibly: regenerate with --emit and // update LChatCore (MessagePayload / Crypto.swift) to match in the same change. import { readFileSync, writeFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; import { generateKey, importKey, encrypt, decrypt } from "../web/static/crypto.js"; const here = dirname(fileURLToPath(import.meta.url)); const fixturePath = join( here, "..", "ios", "LChatCore", "Tests", "LChatCoreTests", "Fixtures", "message-payload-vector.json" ); // The full envelope shape, every optional field populated — so the Swift side // proves it decodes replies, attachment dimensions, ts and uid, not just text. const payload = { text: "Hello from the vector! 🎉", name: "Paul", uid: "u_vector", ts: 1751830000000, attachments: [ { id: "blob123", name: "photo.jpg", type: "image/jpeg", w: 800, h: 600, size: 123456 }, { id: "blob456", name: "clip.mp4", type: "video/mp4", w: 1280, h: 720, size: 9876543, poster: "blob457", dur: 12.5 }, ], replyTo: { id: 41, name: "Ada", snippet: "the original line" }, }; function assert(cond, msg) { if (!cond) { console.error("FAIL:", msg); process.exit(1); } } // `node test/messageblob.test.mjs --emit` regenerates the committed fixture // (encryption picks a fresh IV, so the fixture must be frozen, not recomputed). async function emit() { const { key, b64 } = await generateKey(); const blob = await encrypt(key, payload); const fixture = { _comment: "Message-payload test vector. Generated by test/messageblob.test.mjs from " + "web/static/crypto.js. LChatCore's MessagePayloadVectorTests decrypts `blob` " + "with `keyB64` and must decode every `expect` field — a cross-implementation " + "gate so native message decryption never drifts from the web's. " + "Regenerate with: node test/messageblob.test.mjs --emit", keyB64: b64, blob, expect: payload, }; writeFileSync(fixturePath, JSON.stringify(fixture, null, 2) + "\n"); console.log("wrote", fixturePath); } async function test() { // (1) crypto.js self-consistency on this payload shape. const { key } = await generateKey(); const round = await decrypt(key, await encrypt(key, payload)); assert(JSON.stringify(round) === JSON.stringify(payload), "fresh encrypt/decrypt round-trips"); // (2) the committed fixture (shared with the Swift test) still decrypts. const fx = JSON.parse(readFileSync(fixturePath, "utf8")); const got = await decrypt(await importKey(fx.keyB64), fx.blob); assert(JSON.stringify(got) === JSON.stringify(fx.expect), "committed fixture decrypts to expected payload"); console.log("messageblob.test.mjs OK"); } if (process.argv.includes("--emit")) await emit(); else await test();