import { describe, expect, it } from "vitest"; import { generateKeyPairFromSeed } from "@stablelib/x25519"; import { initHash as nkInitHash, hkdf2 as nkHkdf2 } from "./noise-js"; // PCH-01-002: the Noise handshakes derive `h` (protocol-name init hash) and the // HKDF in MixKey/Split over BLAKE2s, matching the declared pattern name. These // are KNOWN-ANSWER tests over a fully deterministic handshake (fixed statics + // seeded ephemerals): a revert to SHA-256 changes every derived byte, so the // pinned handshakeHash below stops matching and the test fails. That failure is // the intended signal of an accidental hash-algorithm change. function memoryPair() { const a: Uint8Array[] = []; const b: Uint8Array[] = []; const aR: Array<(v: Uint8Array) => void> = []; const bR: Array<(v: Uint8Array) => void> = []; const tA = { async send(m: Uint8Array) { const r = bR.shift(); if (r) r(m); else b.push(m); }, receive(): Promise { const v = a.shift(); if (v) return Promise.resolve(v); return new Promise((resolve) => aR.push(resolve)); }, }; const tB = { async send(m: Uint8Array) { const r = aR.shift(); if (r) r(m); else a.push(m); }, receive(): Promise { const v = b.shift(); if (v) return Promise.resolve(v); return new Promise((resolve) => bR.push(resolve)); }, }; return [tA, tB] as const; } function hex(b: Uint8Array): string { return Array.from(b, (x) => x.toString(16).padStart(2, "0")).join(""); } // Cross-repo frozen vectors for the two changed primitives. These exact hex // values are ALSO pinned in peach-android's CrossPlatformKATTest.kt — a mismatch // on either side means the BLAKE2s wire derivations have diverged. describe("PCH-01-002 — BLAKE2s primitive vectors (shared with peach-android)", () => { const CK = new Uint8Array(32).map((_, i) => i); // 0x00..0x1f const IKM = new Uint8Array(32).map((_, i) => i + 32); // 0x20..0x3f it("initHash is BLAKE2s-256 of the NK protocol name", () => { expect(hex(nkInitHash())).toBe( "e01688d376ef280a27a8af5c559024a02017585b3763763e4f3c7118a979b09e", ); }); it("hkdf2 (HKDF-BLAKE2s) matches the frozen vector", () => { const [k1, k2] = nkHkdf2(CK, IKM); expect(hex(k1)).toBe("6a96444e20e8d4c1cee974416acae1c10b3c92886010e54ed94dafb2c3b80ea0"); expect(hex(k2)).toBe("57af120b0de7acbe7907ec149c5ae870a2dbb74232b65777ba4123f1f7f888f5"); }); }); describe("PCH-01-002 — Noise handshakes derive over BLAKE2s", () => { it("noise_nk_handshake_hash_is_blake2s_derived", async () => { const { noiseNKInitiator, noiseNKResponder } = await import("./noise-js"); const responder = generateKeyPairFromSeed(new Uint8Array(32).fill(0xc3)); const initiatorEphemeral = generateKeyPairFromSeed(new Uint8Array(32).fill(0x01)); const responderEphemeral = generateKeyPairFromSeed(new Uint8Array(32).fill(0x02)); const [tA, tB] = memoryPair(); const [a, b] = await Promise.all([ noiseNKInitiator( responder.publicKey, tA.send, tA.receive, initiatorEphemeral, ), noiseNKResponder( responder.secretKey, responder.publicKey, tB.send, tB.receive, responderEphemeral, ), ]); expect(hex(a.handshakeHash)).toBe(hex(b.handshakeHash)); expect(hex(a.handshakeHash)).toBe( "76f1402ea3ae01615a226b1c95a5d4f4c62eac356d1d5fe282b3417fcb01665f", ); const ct = a.encrypt(new TextEncoder().encode("ping")); expect(new TextDecoder().decode(b.decrypt(ct))).toBe("ping"); }); });