noise-xx.test.ts

3.4 KB · raw

1import { describe, expect, it } from "vitest";2import { generateKeyPair } from "@stablelib/x25519";3import { noiseXXInitiator, noiseXXResponder } from "./noise-xx";45function memoryPair() {6  const a: Uint8Array[] = [];7  const b: Uint8Array[] = [];8  const aR: Array<(v: Uint8Array) => void> = [];9  const bR: Array<(v: Uint8Array) => void> = [];10  const tA = {11    async send(m: Uint8Array) {12      const r = bR.shift();13      if (r) r(m); else b.push(m);14    },15    receive(): Promise<Uint8Array> {16      const v = a.shift();17      if (v) return Promise.resolve(v);18      return new Promise<Uint8Array>((resolve) => aR.push(resolve));19    },20  };21  const tB = {22    async send(m: Uint8Array) {23      const r = aR.shift();24      if (r) r(m); else a.push(m);25    },26    receive(): Promise<Uint8Array> {27      const v = b.shift();28      if (v) return Promise.resolve(v);29      return new Promise<Uint8Array>((resolve) => bR.push(resolve));30    },31  };32  return [tA, tB] as const;33}3435describe("Noise XX mutual authentication", () => {36  it("completes the three-message handshake and exchanges static keys", async () => {37    const alice = generateKeyPair();38    const bob = generateKeyPair();39    const [tA, tB] = memoryPair();4041    const aPromise = noiseXXInitiator(alice.secretKey, alice.publicKey, tA.send, tA.receive);42    const bPromise = noiseXXResponder(bob.secretKey, bob.publicKey, tB.send, tB.receive);4344    const [a, b] = await Promise.all([aPromise, bPromise]);45    expect(a.remoteStaticPublicKey).toEqual(bob.publicKey);46    expect(b.remoteStaticPublicKey).toEqual(alice.publicKey);47    expect(a.handshakeHash).toEqual(b.handshakeHash);48  });4950  it("transport frames decrypt in both directions and are bound to the handshake", async () => {51    const alice = generateKeyPair();52    const bob = generateKeyPair();53    const [tA, tB] = memoryPair();5455    const [a, b] = await Promise.all([56      noiseXXInitiator(alice.secretKey, alice.publicKey, tA.send, tA.receive),57      noiseXXResponder(bob.secretKey, bob.publicKey, tB.send, tB.receive),58    ]);5960    const ct1 = a.encrypt(new TextEncoder().encode("hello bob"));61    expect(new TextDecoder().decode(b.decrypt(ct1))).toBe("hello bob");6263    const ct2 = b.encrypt(new TextEncoder().encode("hello alice"));64    expect(new TextDecoder().decode(a.decrypt(ct2))).toBe("hello alice");6566    // Tampering with one byte breaks the MAC.67    const ct3 = a.encrypt(new TextEncoder().encode("not delivered"));68    ct3[0] ^= 1;69    expect(() => b.decrypt(ct3)).toThrow();70  });7172  it("an attacker who swaps the responder static cannot complete msg2 auth", async () => {73    const alice = generateKeyPair();74    const bobReal = generateKeyPair();75    const bobImpostor = generateKeyPair();76    const [tA, tB] = memoryPair();7778    // Responder uses impostor private key but advertises real pubkey to initiator.79    // Real XX has no out-of-band pubkey check; this test verifies the registry80    // check that the caller MUST perform on `remoteStaticPublicKey`.81    const aPromise = noiseXXInitiator(alice.secretKey, alice.publicKey, tA.send, tA.receive);82    const bPromise = noiseXXResponder(bobImpostor.secretKey, bobImpostor.publicKey, tB.send, tB.receive);83    const [a] = await Promise.all([aPromise, bPromise]);84    // The "swap" is the caller seeing a remote static they did not expect.85    expect(a.remoteStaticPublicKey).not.toEqual(bobReal.publicKey);86    expect(a.remoteStaticPublicKey).toEqual(bobImpostor.publicKey);87  });88});