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 RESPONDER_PRIVATE = new Uint8Array(32).fill(0x02);25const NONCE = new Uint8Array(32).fill(0x03);2627const EXPECTED_INITIATOR_PUBLIC =28 "a4e09292b651c278b9772c569f5fa9bb13d906b46ab68c9df9dc2b4409f8a209";29const EXPECTED_RESPONDER_PUBLIC =30 "ce8d3ad1ccb633ec7b70c17814a5c76ecd029685050d344745ba05870e587d59";31const EXPECTED_PROOF =32 "39efee52ccc9d7eaa24fcfec9a4c75eaad75a6bf3319d9794d9227f2b1466752";3334function derivePublic(privateKey: Uint8Array): Uint8Array {35 const pair = x25519GenerateKeyPair();36 37 38 39 void pair;40 throw new Error("derivePublic should not be called — use pinned constants");41}4243describe("noise-js cross-check", () => {44 it("computes the pinned HMAC-SHA256 proof for the known input vector", () => {45 46 47 48 49 void derivePublic;5051 const initiatorPublic = hexToBytes(EXPECTED_INITIATOR_PUBLIC);52 const responderPublic = hexToBytes(EXPECTED_RESPONDER_PUBLIC);5354 const proof = computeAuthProof(55 INITIATOR_PRIVATE,56 initiatorPublic,57 responderPublic,58 NONCE,59 );6061 expect(bytesToHex(proof)).toBe(EXPECTED_PROOF);62 });63});6465describe("noise NK responder ↔ initiator round trip (pure JS)", () => {66 it("completes handshake, encrypts/decrypts, and verifies auth proof", async () => {67 const initiatorStatic = x25519GenerateKeyPair();68 const responderStatic = x25519GenerateKeyPair();6970 const initToResp: Uint8Array[] = [];71 const respToInit: Uint8Array[] = [];7273 const initSend = async (b: Uint8Array) => { initToResp.push(b); };74 const initRecv = async () => {75 while (respToInit.length === 0) await new Promise<void>((r) => setTimeout(r, 0));76 return respToInit.shift()!;77 };78 const respSend = async (b: Uint8Array) => { respToInit.push(b); };79 const respRecv = async () => {80 while (initToResp.length === 0) await new Promise<void>((r) => setTimeout(r, 0));81 return initToResp.shift()!;82 };8384 const [initSession, respSession] = await Promise.all([85 noiseNKInitiator(responderStatic.publicKey, initSend, initRecv),86 noiseNKResponder(responderStatic.secretKey, responderStatic.publicKey, respSend, respRecv),87 ]);8889 90 const initiatorOut = initSession.encrypt(new TextEncoder().encode("hello from initiator"));91 expect(new TextDecoder().decode(respSession.decrypt(initiatorOut))).toBe("hello from initiator");92 const responderOut = respSession.encrypt(new TextEncoder().encode("hello from responder"));93 expect(new TextDecoder().decode(initSession.decrypt(responderOut))).toBe("hello from responder");9495 96 const initiatorEncSend = async (data: Uint8Array) => { initToResp.push(initSession.encrypt(data)); };97 const initiatorEncRecv = async () => initSession.decrypt(await initRecv());98 const responderEncSend = async (data: Uint8Array) => { respToInit.push(respSession.encrypt(data)); };99 const responderEncRecv = async () => respSession.decrypt(await respRecv());100101 const [, authResult] = await Promise.all([102 performInitiatorAuth(103 initSession,104 initiatorStatic.secretKey,105 initiatorStatic.publicKey,106 responderStatic.publicKey,107 initiatorEncSend,108 initiatorEncRecv,109 ),110 performResponderAuth(111 respSession,112 responderStatic.secretKey,113 responderStatic.publicKey,114 responderEncSend,115 responderEncRecv,116 ),117 ]);118119 expect(bytesToHex(authResult.initiatorPublicKey)).toBe(bytesToHex(initiatorStatic.publicKey));120 });121122 it("rejects a forged proof (constant-time MAC compare)", async () => {123 const initiatorStatic = x25519GenerateKeyPair();124 const attackerStatic = x25519GenerateKeyPair();125 const responderStatic = x25519GenerateKeyPair();126127 const initToResp: Uint8Array[] = [];128 const respToInit: Uint8Array[] = [];129 const initSend = async (b: Uint8Array) => { initToResp.push(b); };130 const initRecv = async () => {131 while (respToInit.length === 0) await new Promise<void>((r) => setTimeout(r, 0));132 return respToInit.shift()!;133 };134 const respSend = async (b: Uint8Array) => { respToInit.push(b); };135 const respRecv = async () => {136 while (initToResp.length === 0) await new Promise<void>((r) => setTimeout(r, 0));137 return initToResp.shift()!;138 };139140 const [initSession, respSession] = await Promise.all([141 noiseNKInitiator(responderStatic.publicKey, initSend, initRecv),142 noiseNKResponder(responderStatic.secretKey, responderStatic.publicKey, respSend, respRecv),143 ]);144145 146 const initiatorEncSend = async (data: Uint8Array) => { initToResp.push(initSession.encrypt(data)); };147 const initiatorEncRecv = async () => initSession.decrypt(await initRecv());148 const responderEncSend = async (data: Uint8Array) => { respToInit.push(respSession.encrypt(data)); };149 const responderEncRecv = async () => respSession.decrypt(await respRecv());150151 const initiatorPromise = performInitiatorAuth(152 initSession,153 attackerStatic.secretKey, 154 initiatorStatic.publicKey, 155 responderStatic.publicKey,156 initiatorEncSend,157 initiatorEncRecv,158 );159 const responderPromise = performResponderAuth(160 respSession,161 responderStatic.secretKey,162 responderStatic.publicKey,163 responderEncSend,164 responderEncRecv,165 );166167 await expect(Promise.all([initiatorPromise, responderPromise])).rejects.toThrow(/proof did not verify/);168 });169});170171function hexToBytes(hex: string): Uint8Array {172 const out = new Uint8Array(hex.length / 2);173 for (let i = 0; i < hex.length; i += 2) {174 out[i / 2] = parseInt(hex.slice(i, i + 2), 16);175 }176 return out;177}