12345678910import { describe, expect, it } from "vitest";11import { generateKeyPair as x25519GenerateKeyPair } from "@stablelib/x25519";1213import {14 bytesToHex,15 computeAuthProof,16 noiseNKInitiator,17 noiseNKResponder,18 performInitiatorAuth,19 performResponderAuth,20} from "./noise-js";212223const INITIATOR_PRIVATE = new Uint8Array(32).fill(0x01);24const NONCE = new Uint8Array(32).fill(0x03);2526const HANDSHAKE_HASH = new Uint8Array(32).fill(0x04);2728const EXPECTED_INITIATOR_PUBLIC =29 "a4e09292b651c278b9772c569f5fa9bb13d906b46ab68c9df9dc2b4409f8a209";30const EXPECTED_RESPONDER_PUBLIC =31 "ce8d3ad1ccb633ec7b70c17814a5c76ecd029685050d344745ba05870e587d59";3233const EXPECTED_PROOF =34 "d947d11a82892a65fdde94da5bbe6ca6034cf1f7e3268fc3ce5851b55f5813ce";3536function derivePublic(_privateKey: Uint8Array): Uint8Array {37 const pair = x25519GenerateKeyPair();38 39 40 41 void pair;42 throw new Error("derivePublic should not be called — use pinned constants");43}4445describe("noise-js cross-check", () => {46 it("computes the pinned HMAC-SHA256 proof for the known input vector", () => {47 48 49 50 51 void derivePublic;5253 const initiatorPublic = hexToBytes(EXPECTED_INITIATOR_PUBLIC);54 const responderPublic = hexToBytes(EXPECTED_RESPONDER_PUBLIC);5556 const proof = computeAuthProof(57 INITIATOR_PRIVATE,58 initiatorPublic,59 responderPublic,60 NONCE,61 HANDSHAKE_HASH,62 );6364 expect(bytesToHex(proof)).toBe(EXPECTED_PROOF);65 });66});6768describe("noise NK responder ↔ initiator round trip (pure JS)", () => {69 it("completes handshake, encrypts/decrypts, and verifies auth proof", async () => {70 const initiatorStatic = x25519GenerateKeyPair();71 const responderStatic = x25519GenerateKeyPair();7273 const initToResp: Uint8Array[] = [];74 const respToInit: Uint8Array[] = [];7576 const initSend = async (b: Uint8Array) => { initToResp.push(b); };77 const initRecv = async () => {78 while (respToInit.length === 0) await new Promise<void>((r) => setTimeout(r, 0));79 return respToInit.shift()!;80 };81 const respSend = async (b: Uint8Array) => { respToInit.push(b); };82 const respRecv = async () => {83 while (initToResp.length === 0) await new Promise<void>((r) => setTimeout(r, 0));84 return initToResp.shift()!;85 };8687 const [initSession, respSession] = await Promise.all([88 noiseNKInitiator(responderStatic.publicKey, initSend, initRecv),89 noiseNKResponder(responderStatic.secretKey, responderStatic.publicKey, respSend, respRecv),90 ]);9192 93 const initiatorOut = initSession.encrypt(new TextEncoder().encode("hello from initiator"));94 expect(new TextDecoder().decode(respSession.decrypt(initiatorOut))).toBe("hello from initiator");95 const responderOut = respSession.encrypt(new TextEncoder().encode("hello from responder"));96 expect(new TextDecoder().decode(initSession.decrypt(responderOut))).toBe("hello from responder");9798 99 const initiatorEncSend = async (data: Uint8Array) => { initToResp.push(initSession.encrypt(data)); };100 const initiatorEncRecv = async () => initSession.decrypt(await initRecv());101 const responderEncSend = async (data: Uint8Array) => { respToInit.push(respSession.encrypt(data)); };102 const responderEncRecv = async () => respSession.decrypt(await respRecv());103104 const [, authResult] = await Promise.all([105 performInitiatorAuth(106 initSession,107 initiatorStatic.secretKey,108 initiatorStatic.publicKey,109 responderStatic.publicKey,110 initiatorEncSend,111 initiatorEncRecv,112 ),113 performResponderAuth(114 respSession,115 responderStatic.secretKey,116 responderStatic.publicKey,117 responderEncSend,118 responderEncRecv,119 ),120 ]);121122 expect(bytesToHex(authResult.initiatorPublicKey)).toBe(bytesToHex(initiatorStatic.publicKey));123 });124125 it("rejects a forged proof (constant-time MAC compare)", async () => {126 const initiatorStatic = x25519GenerateKeyPair();127 const attackerStatic = x25519GenerateKeyPair();128 const responderStatic = x25519GenerateKeyPair();129130 const initToResp: Uint8Array[] = [];131 const respToInit: Uint8Array[] = [];132 const initSend = async (b: Uint8Array) => { initToResp.push(b); };133 const initRecv = async () => {134 while (respToInit.length === 0) await new Promise<void>((r) => setTimeout(r, 0));135 return respToInit.shift()!;136 };137 const respSend = async (b: Uint8Array) => { respToInit.push(b); };138 const respRecv = async () => {139 while (initToResp.length === 0) await new Promise<void>((r) => setTimeout(r, 0));140 return initToResp.shift()!;141 };142143 const [initSession, respSession] = await Promise.all([144 noiseNKInitiator(responderStatic.publicKey, initSend, initRecv),145 noiseNKResponder(responderStatic.secretKey, responderStatic.publicKey, respSend, respRecv),146 ]);147148 149 const initiatorEncSend = async (data: Uint8Array) => { initToResp.push(initSession.encrypt(data)); };150 const initiatorEncRecv = async () => initSession.decrypt(await initRecv());151 const responderEncSend = async (data: Uint8Array) => { respToInit.push(respSession.encrypt(data)); };152 const responderEncRecv = async () => respSession.decrypt(await respRecv());153154 const initiatorPromise = performInitiatorAuth(155 initSession,156 attackerStatic.secretKey, 157 initiatorStatic.publicKey, 158 responderStatic.publicKey,159 initiatorEncSend,160 initiatorEncRecv,161 );162 const responderPromise = performResponderAuth(163 respSession,164 responderStatic.secretKey,165 responderStatic.publicKey,166 responderEncSend,167 responderEncRecv,168 );169170 await expect(Promise.all([initiatorPromise, responderPromise])).rejects.toThrow(/proof did not verify/);171 });172});173174describe("PCH-01-003b — NK auth proof binds the Noise handshake hash", () => {175 176 177 function plaintextChannels() {178 const initToResp: Uint8Array[] = [];179 const respToInit: Uint8Array[] = [];180 const drain = async (q: Uint8Array[]) => {181 while (q.length === 0) await new Promise<void>((r) => setTimeout(r, 0));182 return q.shift()!;183 };184 return {185 initiatorEncSend: async (d: Uint8Array) => { initToResp.push(d); },186 initiatorEncRecv: async () => drain(respToInit),187 responderEncSend: async (d: Uint8Array) => { respToInit.push(d); },188 responderEncRecv: async () => drain(initToResp),189 };190 }191192 it("auth_proof_roundtrips_with_handshake_hash_binding", async () => {193 const initiatorStatic = x25519GenerateKeyPair();194 const responderStatic = x25519GenerateKeyPair();195 const hash = new Uint8Array(32).fill(0x07);196 const ch = plaintextChannels();197198 const [, authResult] = await Promise.all([199 performInitiatorAuth(200 { handshakeHash: hash } as never,201 initiatorStatic.secretKey,202 initiatorStatic.publicKey,203 responderStatic.publicKey,204 ch.initiatorEncSend,205 ch.initiatorEncRecv,206 ),207 performResponderAuth(208 { handshakeHash: hash } as never,209 responderStatic.secretKey,210 responderStatic.publicKey,211 ch.responderEncSend,212 ch.responderEncRecv,213 ),214 ]);215216 expect(bytesToHex(authResult.initiatorPublicKey)).toBe(bytesToHex(initiatorStatic.publicKey));217 });218219 it("auth_proof_rejected_when_handshake_hash_differs", async () => {220 const initiatorStatic = x25519GenerateKeyPair();221 const responderStatic = x25519GenerateKeyPair();222 const ch = plaintextChannels();223 224 225 const initiatorPromise = performInitiatorAuth(226 { handshakeHash: new Uint8Array(32).fill(0x07) } as never,227 initiatorStatic.secretKey,228 initiatorStatic.publicKey,229 responderStatic.publicKey,230 ch.initiatorEncSend,231 ch.initiatorEncRecv,232 );233 const responderPromise = performResponderAuth(234 { handshakeHash: new Uint8Array(32).fill(0x09) } as never,235 responderStatic.secretKey,236 responderStatic.publicKey,237 ch.responderEncSend,238 ch.responderEncRecv,239 );240241 await expect(Promise.all([initiatorPromise, responderPromise])).rejects.toThrow(/proof did not verify/);242 });243});244245function hexToBytes(hex: string): Uint8Array {246 const out = new Uint8Array(hex.length / 2);247 for (let i = 0; i < hex.length; i += 2) {248 out[i / 2] = parseInt(hex.slice(i, i + 2), 16);249 }250 return out;251}