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 EXPECTED_INITIATOR_PUBLIC =27 "a4e09292b651c278b9772c569f5fa9bb13d906b46ab68c9df9dc2b4409f8a209";28const EXPECTED_RESPONDER_PUBLIC =29 "ce8d3ad1ccb633ec7b70c17814a5c76ecd029685050d344745ba05870e587d59";30const EXPECTED_PROOF =31 "39efee52ccc9d7eaa24fcfec9a4c75eaad75a6bf3319d9794d9227f2b1466752";3233function derivePublic(_privateKey: Uint8Array): Uint8Array {34 const pair = x25519GenerateKeyPair();35 36 37 38 void pair;39 throw new Error("derivePublic should not be called — use pinned constants");40}4142describe("noise-js cross-check", () => {43 it("computes the pinned HMAC-SHA256 proof for the known input vector", () => {44 45 46 47 48 void derivePublic;4950 const initiatorPublic = hexToBytes(EXPECTED_INITIATOR_PUBLIC);51 const responderPublic = hexToBytes(EXPECTED_RESPONDER_PUBLIC);5253 const proof = computeAuthProof(54 INITIATOR_PRIVATE,55 initiatorPublic,56 responderPublic,57 NONCE,58 );5960 expect(bytesToHex(proof)).toBe(EXPECTED_PROOF);61 });62});6364describe("noise NK responder ↔ initiator round trip (pure JS)", () => {65 it("completes handshake, encrypts/decrypts, and verifies auth proof", async () => {66 const initiatorStatic = x25519GenerateKeyPair();67 const responderStatic = x25519GenerateKeyPair();6869 const initToResp: Uint8Array[] = [];70 const respToInit: Uint8Array[] = [];7172 const initSend = async (b: Uint8Array) => { initToResp.push(b); };73 const initRecv = async () => {74 while (respToInit.length === 0) await new Promise<void>((r) => setTimeout(r, 0));75 return respToInit.shift()!;76 };77 const respSend = async (b: Uint8Array) => { respToInit.push(b); };78 const respRecv = async () => {79 while (initToResp.length === 0) await new Promise<void>((r) => setTimeout(r, 0));80 return initToResp.shift()!;81 };8283 const [initSession, respSession] = await Promise.all([84 noiseNKInitiator(responderStatic.publicKey, initSend, initRecv),85 noiseNKResponder(responderStatic.secretKey, responderStatic.publicKey, respSend, respRecv),86 ]);8788 89 const initiatorOut = initSession.encrypt(new TextEncoder().encode("hello from initiator"));90 expect(new TextDecoder().decode(respSession.decrypt(initiatorOut))).toBe("hello from initiator");91 const responderOut = respSession.encrypt(new TextEncoder().encode("hello from responder"));92 expect(new TextDecoder().decode(initSession.decrypt(responderOut))).toBe("hello from responder");9394 95 const initiatorEncSend = async (data: Uint8Array) => { initToResp.push(initSession.encrypt(data)); };96 const initiatorEncRecv = async () => initSession.decrypt(await initRecv());97 const responderEncSend = async (data: Uint8Array) => { respToInit.push(respSession.encrypt(data)); };98 const responderEncRecv = async () => respSession.decrypt(await respRecv());99100 const [, authResult] = await Promise.all([101 performInitiatorAuth(102 initSession,103 initiatorStatic.secretKey,104 initiatorStatic.publicKey,105 responderStatic.publicKey,106 initiatorEncSend,107 initiatorEncRecv,108 ),109 performResponderAuth(110 respSession,111 responderStatic.secretKey,112 responderStatic.publicKey,113 responderEncSend,114 responderEncRecv,115 ),116 ]);117118 expect(bytesToHex(authResult.initiatorPublicKey)).toBe(bytesToHex(initiatorStatic.publicKey));119 });120121 it("rejects a forged proof (constant-time MAC compare)", async () => {122 const initiatorStatic = x25519GenerateKeyPair();123 const attackerStatic = x25519GenerateKeyPair();124 const responderStatic = x25519GenerateKeyPair();125126 const initToResp: Uint8Array[] = [];127 const respToInit: Uint8Array[] = [];128 const initSend = async (b: Uint8Array) => { initToResp.push(b); };129 const initRecv = async () => {130 while (respToInit.length === 0) await new Promise<void>((r) => setTimeout(r, 0));131 return respToInit.shift()!;132 };133 const respSend = async (b: Uint8Array) => { respToInit.push(b); };134 const respRecv = async () => {135 while (initToResp.length === 0) await new Promise<void>((r) => setTimeout(r, 0));136 return initToResp.shift()!;137 };138139 const [initSession, respSession] = await Promise.all([140 noiseNKInitiator(responderStatic.publicKey, initSend, initRecv),141 noiseNKResponder(responderStatic.secretKey, responderStatic.publicKey, respSend, respRecv),142 ]);143144 145 const initiatorEncSend = async (data: Uint8Array) => { initToResp.push(initSession.encrypt(data)); };146 const initiatorEncRecv = async () => initSession.decrypt(await initRecv());147 const responderEncSend = async (data: Uint8Array) => { respToInit.push(respSession.encrypt(data)); };148 const responderEncRecv = async () => respSession.decrypt(await respRecv());149150 const initiatorPromise = performInitiatorAuth(151 initSession,152 attackerStatic.secretKey, 153 initiatorStatic.publicKey, 154 responderStatic.publicKey,155 initiatorEncSend,156 initiatorEncRecv,157 );158 const responderPromise = performResponderAuth(159 respSession,160 responderStatic.secretKey,161 responderStatic.publicKey,162 responderEncSend,163 responderEncRecv,164 );165166 await expect(Promise.all([initiatorPromise, responderPromise])).rejects.toThrow(/proof did not verify/);167 });168});169170function hexToBytes(hex: string): Uint8Array {171 const out = new Uint8Array(hex.length / 2);172 for (let i = 0; i < hex.length; i += 2) {173 out[i / 2] = parseInt(hex.slice(i, i + 2), 16);174 }175 return out;176}