// Cross-implementation gate for ECIES thread-key wrapping. The native client // (ios/LChatCore/Sources/LChatCore/Crypto.swift) reimplements unwrapThreadKey in // CryptoKit; if the web's crypto.js ever changes the wire format (curve, HKDF // salt/info, key encodings), the two silently drift and threads stop opening on // one platform. This test pins the format two ways: // // 1. a fresh wrap → unwrap round-trips in JS (crypto.js self-consistency), and // 2. the committed fixture (LChatCore's Tests/…/Fixtures/thread-key-vector.json) // still unwraps to its expected key — the SAME fixture the Swift unit test // (`swift test` in ios/LChatCore) consumes. // // If (2) fails, crypto.js changed incompatibly: regenerate the fixture (see below) // and update 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 { generateUserKeypair, wrapThreadKey, unwrapThreadKey, bytesToB64url } from "../web/static/crypto.js"; const here = dirname(fileURLToPath(import.meta.url)); const fixturePath = join(here, "..", "ios", "LChatCore", "Tests", "LChatCoreTests", "Fixtures", "thread-key-vector.json"); // A known 32-byte thread key (bytes 0..31), so the expected value is stable. function knownThreadKeyB64() { const raw = new Uint8Array(32); for (let i = 0; i < 32; i++) raw[i] = i; return bytesToB64url(raw); } function assert(cond, msg) { if (!cond) { console.error("FAIL:", msg); process.exit(1); } } // `node test/threadkey.test.mjs --emit` regenerates the committed fixture (the wrap // uses a random ephemeral key, so the fixture must be frozen, not recomputed). async function emit() { const expectedThreadKeyB64 = knownThreadKeyB64(); const kp = await generateUserKeypair(); const wrappedB64 = await wrapThreadKey(expectedThreadKeyB64, kp.publicKey); assert((await unwrapThreadKey(wrappedB64, kp.privateKey)) === expectedThreadKeyB64, "round-trip"); const fixture = { _comment: "ECIES thread-key test vector. Generated by test/threadkey.test.mjs from " + "web/static/crypto.js. LChatCore's ECIESVectorTests unwraps wrappedB64 with " + "privPkcs8B64 and must get expectedThreadKeyB64 — a cross-implementation gate " + "so the native ECIES never drifts from the web's. base64url throughout. " + "Regenerate with: node test/threadkey.test.mjs --emit", privPkcs8B64: kp.privateKey, pubRawB64: kp.publicKey, wrappedB64, expectedThreadKeyB64, }; writeFileSync(fixturePath, JSON.stringify(fixture, null, 2) + "\n"); console.log("wrote", fixturePath); } async function test() { // (1) crypto.js self-consistency. const k = knownThreadKeyB64(); const kp = await generateUserKeypair(); const wrapped = await wrapThreadKey(k, kp.publicKey); assert((await unwrapThreadKey(wrapped, kp.privateKey)) === k, "fresh wrap/unwrap round-trips"); // (2) the committed fixture (shared with the Swift test) still unwraps. const fx = JSON.parse(readFileSync(fixturePath, "utf8")); const got = await unwrapThreadKey(fx.wrappedB64, fx.privPkcs8B64); assert(got === fx.expectedThreadKeyB64, "committed fixture unwraps to expected key"); console.log("threadkey.test.mjs OK"); } if (process.argv.includes("--emit")) await emit(); else await test();