| 1 | // Cross-implementation gate for the per-viewer names blob (#577). The native |
| 2 | // client (ios/LChatCore Nicknames.swift + Crypto.swift seal/open) reimplements |
| 3 | // the web's sealForPub/openSealed for the {v:1, names:{uid:{name,seen}}} blob; |
| 4 | // if either side drifts (wire shape, JSON shape), nicknames stop opening on |
| 5 | // one platform. This pins the format two ways: |
| 6 | // |
| 7 | // 1. a fresh seal → open round-trips in JS (crypto.js self-consistency), and |
| 8 | // 2. the committed fixture (LChatCore's Fixtures/nicknames-vector.json) |
| 9 | // still opens to its expected map — the SAME fixture NicknamesVectorTests |
| 10 | // consumes (it also seals natively and re-opens, covering both directions). |
| 11 | // |
| 12 | // Regenerate with: node test/nicknames.test.mjs --emit |
| 13 | import { readFileSync, writeFileSync } from "node:fs"; |
| 14 | import { fileURLToPath } from "node:url"; |
| 15 | import { dirname, join } from "node:path"; |
| 16 | import { generateUserKeypair, sealForPub, openSealed } from "../web/static/crypto.js"; |
| 17 | |
| 18 | const here = dirname(fileURLToPath(import.meta.url)); |
| 19 | const fixturePath = join(here, "..", "ios", "LChatCore", "Tests", "LChatCoreTests", "Fixtures", "nicknames-vector.json"); |
| 20 | |
| 21 | // A stable map exercising the shapes that matter: a plain rename, a |
| 22 | // multi-word name, and unicode. |
| 23 | const NAMES = { |
| 24 | "uid-dermot": { name: "Dermo", seen: "Dermot" }, |
| 25 | "uid-anna": { name: "Mum", seen: "Anna Lee" }, |
| 26 | "uid-jose": { name: "José 🎉", seen: "Jose" }, |
| 27 | }; |
| 28 | |
| 29 | function assert(cond, msg) { |
| 30 | if (!cond) { |
| 31 | console.error("FAIL:", msg); |
| 32 | process.exit(1); |
| 33 | } |
| 34 | } |
| 35 | |
| 36 | async function emit() { |
| 37 | const kp = await generateUserKeypair(); |
| 38 | const sealedB64 = await sealForPub(kp.publicKey, { v: 1, names: NAMES }); |
| 39 | const opened = await openSealed(sealedB64, kp.privateKey); |
| 40 | assert(JSON.stringify(opened.names) === JSON.stringify(NAMES), "round-trip"); |
| 41 | const fixture = { |
| 42 | _comment: |
| 43 | "Per-viewer names blob test vector (#577). Generated by test/nicknames.test.mjs " + |
| 44 | "from web/static/crypto.js sealForPub. LChatCore's NicknamesVectorTests opens " + |
| 45 | "sealedB64 with privPkcs8B64 and must get expectedNames — and seals its own " + |
| 46 | "blob to pubRawB64, which must open on both sides. base64url throughout. " + |
| 47 | "Regenerate with: node test/nicknames.test.mjs --emit", |
| 48 | privPkcs8B64: kp.privateKey, |
| 49 | pubRawB64: kp.publicKey, |
| 50 | sealedB64, |
| 51 | expectedNames: NAMES, |
| 52 | }; |
| 53 | writeFileSync(fixturePath, JSON.stringify(fixture, null, 2) + "\n"); |
| 54 | console.log("wrote", fixturePath); |
| 55 | } |
| 56 | |
| 57 | async function test() { |
| 58 | // (1) crypto.js self-consistency. |
| 59 | const kp = await generateUserKeypair(); |
| 60 | const sealed = await sealForPub(kp.publicKey, { v: 1, names: NAMES }); |
| 61 | const opened = await openSealed(sealed, kp.privateKey); |
| 62 | assert(JSON.stringify(opened.names) === JSON.stringify(NAMES), "fresh seal round-trips"); |
| 63 | |
| 64 | // (2) the committed fixture still opens — the same bytes Swift consumes. |
| 65 | const fx = JSON.parse(readFileSync(fixturePath, "utf8")); |
| 66 | const fromFixture = await openSealed(fx.sealedB64, fx.privPkcs8B64); |
| 67 | assert( |
| 68 | JSON.stringify(fromFixture.names) === JSON.stringify(fx.expectedNames), |
| 69 | "committed fixture opens to expectedNames", |
| 70 | ); |
| 71 | console.log("ALL PASSED"); |
| 72 | } |
| 73 | |
| 74 | if (process.argv.includes("--emit")) await emit(); |
| 75 | else await test(); |
| 76 | |