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 initiatorEphemeral = generateKeyPairFromSeed(new Uint8Array(32).fill(0x03));100 const responderEphemeral = generateKeyPairFromSeed(new Uint8Array(32).fill(0x04));101 const wire = channels();102 const [a, b] = await Promise.all([103 noiseKKInitiator(104 initiator.secretKey,105 initiator.publicKey,106 responder.publicKey,107 wire.initiatorSend,108 wire.initiatorReceive,109 initiatorEphemeral,110 ),111 noiseKKResponder(112 responder.secretKey,113 responder.publicKey,114 initiator.publicKey,115 wire.responderSend,116 wire.responderReceive,117 responderEphemeral,118 ),119 ]);120121 expect(bytesToHex(a.handshakeHash)).toBe(122 "dced1b411d7dc25716d8fc7fdc6b49418ae5648f323df5fb20a105f3c0acabab",123 );124 expect(bytesToHex(b.handshakeHash)).toBe(bytesToHex(a.handshakeHash));125 });126127 it("hard-fails a mismatched expected initiator key without downgrade", async () => {128 const initiator = generateKeyPair();129 const attacker = generateKeyPair();130 const responder = generateKeyPair();131 const wire = channels();132133 const initiatorHandshake = noiseKKInitiator(134 initiator.secretKey,135 initiator.publicKey,136 responder.publicKey,137 wire.initiatorSend,138 wire.initiatorReceive,139 );140 const responderHandshake = noiseKKResponder(141 responder.secretKey,142 responder.publicKey,143 attacker.publicKey,144 wire.responderSend,145 wire.responderReceive,146 );147148 await expect(Promise.all([initiatorHandshake, responderHandshake])).rejects.toThrow(149 /decrypt/i,150 );151 });152});