12345678910111213141516import { describe, expect, it } from "vitest";17import { generateKeyPair as x25519GenerateKeyPair } from "@stablelib/x25519";1819import {20 bytesToHex,21 computeAuthProof,22 noiseNKInitiator,23 noiseNKResponder,24 performInitiatorAuth,25 performResponderAuth,26} from "./noise-js";272829const INITIATOR_PRIVATE = new Uint8Array(32).fill(0x01);30const RESPONDER_PRIVATE = new Uint8Array(32).fill(0x02);31const NONCE = new Uint8Array(32).fill(0x03);3233const EXPECTED_INITIATOR_PUBLIC =34 "a4e09292b651c278b9772c569f5fa9bb13d906b46ab68c9df9dc2b4409f8a209";35const EXPECTED_RESPONDER_PUBLIC =36 "ce8d3ad1ccb633ec7b70c17814a5c76ecd029685050d344745ba05870e587d59";37const EXPECTED_PROOF =38 "39efee52ccc9d7eaa24fcfec9a4c75eaad75a6bf3319d9794d9227f2b1466752";3940function derivePublic(privateKey: Uint8Array): Uint8Array {41 const pair = x25519GenerateKeyPair();42 43 44 45 void pair;46 throw new Error("derivePublic should not be called — use pinned constants");47}4849describe("noise-js cross-check", () => {50 it("computes the same proof bytes as the Rust auth.rs HMAC-SHA256 scheme", () => {51 52 53 54 55 void derivePublic;5657 const initiatorPublic = hexToBytes(EXPECTED_INITIATOR_PUBLIC);58 const responderPublic = hexToBytes(EXPECTED_RESPONDER_PUBLIC);5960 const proof = computeAuthProof(61 INITIATOR_PRIVATE,62 initiatorPublic,63 responderPublic,64 NONCE,65 );6667 expect(bytesToHex(proof)).toBe(EXPECTED_PROOF);68 });69});7071describe("noise NK responder ↔ initiator round trip (pure JS)", () => {72 it("completes handshake, encrypts/decrypts, and verifies auth proof", async () => {73 const initiatorStatic = x25519GenerateKeyPair();74 const responderStatic = x25519GenerateKeyPair();7576 const initToResp: Uint8Array[] = [];77 const respToInit: Uint8Array[] = [];7879 const initSend = async (b: Uint8Array) => { initToResp.push(b); };80 const initRecv = async () => {81 while (respToInit.length === 0) await new Promise<void>((r) => setTimeout(r, 0));82 return respToInit.shift()!;83 };84 const respSend = async (b: Uint8Array) => { respToInit.push(b); };85 const respRecv = async () => {86 while (initToResp.length === 0) await new Promise<void>((r) => setTimeout(r, 0));87 return initToResp.shift()!;88 };8990 const [initSession, respSession] = await Promise.all([91 noiseNKInitiator(responderStatic.publicKey, initSend, initRecv),92 noiseNKResponder(responderStatic.secretKey, responderStatic.publicKey, respSend, respRecv),93 ]);9495 96 const initiatorOut = initSession.encrypt(new TextEncoder().encode("hello from initiator"));97 expect(new TextDecoder().decode(respSession.decrypt(initiatorOut))).toBe("hello from initiator");98 const responderOut = respSession.encrypt(new TextEncoder().encode("hello from responder"));99 expect(new TextDecoder().decode(initSession.decrypt(responderOut))).toBe("hello from responder");100101 102 const initiatorEncSend = async (data: Uint8Array) => { initToResp.push(initSession.encrypt(data)); };103 const initiatorEncRecv = async () => initSession.decrypt(await initRecv());104 const responderEncSend = async (data: Uint8Array) => { respToInit.push(respSession.encrypt(data)); };105 const responderEncRecv = async () => respSession.decrypt(await respRecv());106107 const [, authResult] = await Promise.all([108 performInitiatorAuth(109 initSession,110 initiatorStatic.secretKey,111 initiatorStatic.publicKey,112 responderStatic.publicKey,113 initiatorEncSend,114 initiatorEncRecv,115 ),116 performResponderAuth(117 respSession,118 responderStatic.secretKey,119 responderStatic.publicKey,120 responderEncSend,121 responderEncRecv,122 ),123 ]);124125 expect(bytesToHex(authResult.initiatorPublicKey)).toBe(bytesToHex(initiatorStatic.publicKey));126 });127128 it("rejects a forged proof (constant-time MAC compare)", async () => {129 const initiatorStatic = x25519GenerateKeyPair();130 const attackerStatic = x25519GenerateKeyPair();131 const responderStatic = x25519GenerateKeyPair();132133 const initToResp: Uint8Array[] = [];134 const respToInit: Uint8Array[] = [];135 const initSend = async (b: Uint8Array) => { initToResp.push(b); };136 const initRecv = async () => {137 while (respToInit.length === 0) await new Promise<void>((r) => setTimeout(r, 0));138 return respToInit.shift()!;139 };140 const respSend = async (b: Uint8Array) => { respToInit.push(b); };141 const respRecv = async () => {142 while (initToResp.length === 0) await new Promise<void>((r) => setTimeout(r, 0));143 return initToResp.shift()!;144 };145146 const [initSession, respSession] = await Promise.all([147 noiseNKInitiator(responderStatic.publicKey, initSend, initRecv),148 noiseNKResponder(responderStatic.secretKey, responderStatic.publicKey, respSend, respRecv),149 ]);150151 152 const initiatorEncSend = async (data: Uint8Array) => { initToResp.push(initSession.encrypt(data)); };153 const initiatorEncRecv = async () => initSession.decrypt(await initRecv());154 const responderEncSend = async (data: Uint8Array) => { respToInit.push(respSession.encrypt(data)); };155 const responderEncRecv = async () => respSession.decrypt(await respRecv());156157 const initiatorPromise = performInitiatorAuth(158 initSession,159 attackerStatic.secretKey, 160 initiatorStatic.publicKey, 161 responderStatic.publicKey,162 initiatorEncSend,163 initiatorEncRecv,164 );165 const responderPromise = performResponderAuth(166 respSession,167 responderStatic.secretKey,168 responderStatic.publicKey,169 responderEncSend,170 responderEncRecv,171 );172173 await expect(Promise.all([initiatorPromise, responderPromise])).rejects.toThrow(/proof did not verify/);174 });175});176177function hexToBytes(hex: string): Uint8Array {178 const out = new Uint8Array(hex.length / 2);179 for (let i = 0; i < hex.length; i += 2) {180 out[i / 2] = parseInt(hex.slice(i, i + 2), 16);181 }182 return out;183}