1import { describe, expect, it } from "vitest";2import { generateKeyPair, generateKeyPairFromSeed } from "@stablelib/x25519";3import {4 bytesToHex,5 noiseKKInitiator,6 noiseKKResponder,7} from "./noise-js";89function channels() {10 const initiatorToResponder: Uint8Array[] = [];11 const responderToInitiator: Uint8Array[] = [];12 const drain = async (queue: Uint8Array[]) => {13 while (queue.length === 0) await new Promise<void>((resolve) => setTimeout(resolve, 0));14 return queue.shift()!;15 };16 return {17 initiatorSend: async (frame: Uint8Array) => { initiatorToResponder.push(frame); },18 initiatorReceive: () => drain(responderToInitiator),19 responderSend: async (frame: Uint8Array) => { responderToInitiator.push(frame); },20 responderReceive: () => drain(initiatorToResponder),21 };22}2324describe("Noise KK reconnect", () => {25 it("mutually authenticates known static keys and carries transport frames", async () => {26 const initiator = generateKeyPair();27 const responder = generateKeyPair();28 const wire = channels();29 const [a, b] = await Promise.all([30 noiseKKInitiator(31 initiator.secretKey,32 initiator.publicKey,33 responder.publicKey,34 wire.initiatorSend,35 wire.initiatorReceive,36 ),37 noiseKKResponder(38 responder.secretKey,39 responder.publicKey,40 initiator.publicKey,41 wire.responderSend,42 wire.responderReceive,43 ),44 ]);4546 expect(bytesToHex(a.handshakeHash)).toBe(bytesToHex(b.handshakeHash));47 expect(new TextDecoder().decode(b.decrypt(a.encrypt(new TextEncoder().encode("one"))))).toBe("one");48 expect(new TextDecoder().decode(a.decrypt(b.encrypt(new TextEncoder().encode("two"))))).toBe("two");49 });5051 it("pins the cross-platform deterministic handshake hash", async () => {52 const initiator = generateKeyPairFromSeed(new Uint8Array(32).fill(0x01));53 const responder = generateKeyPairFromSeed(new Uint8Array(32).fill(0x02));54 const initiatorEphemeral = generateKeyPairFromSeed(new Uint8Array(32).fill(0x03));55 const responderEphemeral = generateKeyPairFromSeed(new Uint8Array(32).fill(0x04));56 const wire = channels();57 const [a, b] = await Promise.all([58 noiseKKInitiator(59 initiator.secretKey,60 initiator.publicKey,61 responder.publicKey,62 wire.initiatorSend,63 wire.initiatorReceive,64 initiatorEphemeral,65 ),66 noiseKKResponder(67 responder.secretKey,68 responder.publicKey,69 initiator.publicKey,70 wire.responderSend,71 wire.responderReceive,72 responderEphemeral,73 ),74 ]);7576 expect(bytesToHex(a.handshakeHash)).toBe(77 "e92392c530684df3eed06a76bd18da5c7a1887e88f5fdee758f9870dadd28182",78 );79 expect(bytesToHex(b.handshakeHash)).toBe(bytesToHex(a.handshakeHash));80 });8182 it("hard-fails a mismatched expected initiator key without downgrade", async () => {83 const initiator = generateKeyPair();84 const attacker = generateKeyPair();85 const responder = generateKeyPair();86 const wire = channels();8788 const initiatorHandshake = noiseKKInitiator(89 initiator.secretKey,90 initiator.publicKey,91 responder.publicKey,92 wire.initiatorSend,93 wire.initiatorReceive,94 );95 const responderHandshake = noiseKKResponder(96 responder.secretKey,97 responder.publicKey,98 attacker.publicKey,99 wire.responderSend,100 wire.responderReceive,101 );102103 await expect(Promise.all([initiatorHandshake, responderHandshake])).rejects.toThrow(104 /Noise KK: msg1 auth failed/,105 );106 });107});