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("E6: every reconnect derives fresh keys, so one session never exposes another", async () => {52 53 54 55 56 57 58 const initiator = generateKeyPair();59 const responder = generateKeyPair();6061 async function handshake() {62 const wire = channels();63 const [a, b] = await Promise.all([64 noiseKKInitiator(65 initiator.secretKey,66 initiator.publicKey,67 responder.publicKey,68 wire.initiatorSend,69 wire.initiatorReceive,70 ),71 noiseKKResponder(72 responder.secretKey,73 responder.publicKey,74 initiator.publicKey,75 wire.responderSend,76 wire.responderReceive,77 ),78 ]);79 return { a, b };80 }8182 const first = await handshake();83 const second = await handshake();8485 86 expect(bytesToHex(first.a.handshakeHash)).not.toBe(87 bytesToHex(second.a.handshakeHash),88 );8990 91 92 const frame = second.a.encrypt(new TextEncoder().encode("session two"));93 expect(() => first.b.decrypt(frame)).toThrow();94 });9596 it("pins the cross-platform deterministic handshake hash", async () => {97 const initiator = generateKeyPairFromSeed(new Uint8Array(32).fill(0x01));98 const responder = generateKeyPairFromSeed(new Uint8Array(32).fill(0x02));99 const wire = channels();100 const [a, b] = await Promise.all([101 noiseKKInitiator(102 initiator.secretKey,103 initiator.publicKey,104 responder.publicKey,105 wire.initiatorSend,106 wire.initiatorReceive,107 ),108 noiseKKResponder(109 responder.secretKey,110 responder.publicKey,111 initiator.publicKey,112 wire.responderSend,113 wire.responderReceive,114 ),115 ]);116117 expect(a.handshakeHash).toHaveLength(32);118 expect(bytesToHex(b.handshakeHash)).toBe(bytesToHex(a.handshakeHash));119 });120121 it("hard-fails a mismatched expected initiator key without downgrade", async () => {122 const initiator = generateKeyPair();123 const attacker = generateKeyPair();124 const responder = generateKeyPair();125 const wire = channels();126127 const initiatorHandshake = noiseKKInitiator(128 initiator.secretKey,129 initiator.publicKey,130 responder.publicKey,131 wire.initiatorSend,132 wire.initiatorReceive,133 );134 const responderHandshake = noiseKKResponder(135 responder.secretKey,136 responder.publicKey,137 attacker.publicKey,138 wire.responderSend,139 wire.responderReceive,140 );141142 await expect(Promise.all([initiatorHandshake, responderHandshake])).rejects.toThrow(143 /decrypt/i,144 );145 });146});