eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1// Cross-implementation gate for ECIES thread-key wrapping. The native client
2// (ios/LChatCore/Sources/LChatCore/Crypto.swift) reimplements unwrapThreadKey in
3// CryptoKit; if the web's crypto.js ever changes the wire format (curve, HKDF
4// salt/info, key encodings), the two silently drift and threads stop opening on
5// one platform. This test pins the format two ways:
6//
7// 1. a fresh wrap → unwrap round-trips in JS (crypto.js self-consistency), and
8// 2. the committed fixture (LChatCore's Tests/…/Fixtures/thread-key-vector.json)
9// still unwraps to its expected key — the SAME fixture the Swift unit test
10// (`swift test` in ios/LChatCore) consumes.
11//
12// If (2) fails, crypto.js changed incompatibly: regenerate the fixture (see below)
13// and update Crypto.swift to match in the same change.
14import { readFileSync, writeFileSync } from "node:fs";
15import { fileURLToPath } from "node:url";
16import { dirname, join } from "node:path";
17import { generateUserKeypair, wrapThreadKey, unwrapThreadKey, bytesToB64url } from "../web/static/crypto.js";
18
19const here = dirname(fileURLToPath(import.meta.url));
20const fixturePath = join(here, "..", "ios", "LChatCore", "Tests", "LChatCoreTests", "Fixtures", "thread-key-vector.json");
21
22// A known 32-byte thread key (bytes 0..31), so the expected value is stable.
23function knownThreadKeyB64() {
24 const raw = new Uint8Array(32);
25 for (let i = 0; i < 32; i++) raw[i] = i;
26 return bytesToB64url(raw);
27}
28
29function assert(cond, msg) {
30 if (!cond) {
31 console.error("FAIL:", msg);
32 process.exit(1);
33 }
34}
35
36// `node test/threadkey.test.mjs --emit` regenerates the committed fixture (the wrap
37// uses a random ephemeral key, so the fixture must be frozen, not recomputed).
38async function emit() {
39 const expectedThreadKeyB64 = knownThreadKeyB64();
40 const kp = await generateUserKeypair();
41 const wrappedB64 = await wrapThreadKey(expectedThreadKeyB64, kp.publicKey);
42 assert((await unwrapThreadKey(wrappedB64, kp.privateKey)) === expectedThreadKeyB64, "round-trip");
43 const fixture = {
44 _comment:
45 "ECIES thread-key test vector. Generated by test/threadkey.test.mjs from " +
46 "web/static/crypto.js. LChatCore's ECIESVectorTests unwraps wrappedB64 with " +
47 "privPkcs8B64 and must get expectedThreadKeyB64 — a cross-implementation gate " +
48 "so the native ECIES never drifts from the web's. base64url throughout. " +
49 "Regenerate with: node test/threadkey.test.mjs --emit",
50 privPkcs8B64: kp.privateKey,
51 pubRawB64: kp.publicKey,
52 wrappedB64,
53 expectedThreadKeyB64,
54 };
55 writeFileSync(fixturePath, JSON.stringify(fixture, null, 2) + "\n");
56 console.log("wrote", fixturePath);
57}
58
59async function test() {
60 // (1) crypto.js self-consistency.
61 const k = knownThreadKeyB64();
62 const kp = await generateUserKeypair();
63 const wrapped = await wrapThreadKey(k, kp.publicKey);
64 assert((await unwrapThreadKey(wrapped, kp.privateKey)) === k, "fresh wrap/unwrap round-trips");
65
66 // (2) the committed fixture (shared with the Swift test) still unwraps.
67 const fx = JSON.parse(readFileSync(fixturePath, "utf8"));
68 const got = await unwrapThreadKey(fx.wrappedB64, fx.privPkcs8B64);
69 assert(got === fx.expectedThreadKeyB64, "committed fixture unwraps to expected key");
70
71 console.log("threadkey.test.mjs OK");
72}
73
74if (process.argv.includes("--emit")) await emit();
75else await test();
76