sync-kk-transport.test.ts

4.4 KB · raw

1import { afterEach, describe, expect, it, vi } from "vitest";2import { generateKeyPair } from "@stablelib/x25519";3import { noiseKKResponder } from "./noise-js";4import { openSyncSession, type SyncPeerIdentity } from "./sync-transport";56const encode = (value: unknown) => new TextEncoder().encode(JSON.stringify(value));7const decode = (bytes: Uint8Array) => JSON.parse(new TextDecoder().decode(bytes));8const hex = (bytes: Uint8Array) =>9  Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");10const RENDEZVOUS_ID = "Q".repeat(43);1112function installPeer(peerIdentityKeyHex?: string) {13  const initiator = generateKeyPair();14  const responder = generateKeyPair();15  const clientFrames: Uint8Array[] = [];16  const clientReaders: Array<(frame: Uint8Array) => void> = [];17  let socket: {18    binaryType: string;19    onopen: ((event: Event) => void) | null;20    onmessage: ((event: MessageEvent) => void) | null;21    onerror: (() => void) | null;22    onclose: ((event: CloseEvent) => void) | null;23    send(data: Uint8Array | ArrayBuffer): void;24    close(): void;25  };26  const receiveClient = async () => {27    const queued = clientFrames.shift();28    if (queued) return queued;29    return new Promise<Uint8Array>((resolve) => clientReaders.push(resolve));30  };31  const sendServer = async (frame: Uint8Array) => {32    queueMicrotask(() => socket.onmessage?.({ data: frame.buffer } as MessageEvent));33  };3435  const selectorSeen = new Promise<Record<string, unknown>>((resolve, reject) => {36    const WebSocketMock = vi.fn().mockImplementation(function() {37      socket = {38        binaryType: "arraybuffer",39        onopen: null,40        onmessage: null,41        onerror: null,42        onclose: null,43        send(data) {44          const frame = data instanceof Uint8Array ? data.slice() : new Uint8Array(data.slice(0));45          const reader = clientReaders.shift();46          if (reader) reader(frame);47          else clientFrames.push(frame);48        },49        close() {50          queueMicrotask(() => socket.onclose?.({ code: 1000 } as CloseEvent));51        },52      };53      queueMicrotask(() => socket.onopen?.(new Event("open")));54      return socket;55    });56    vi.stubGlobal("WebSocket", WebSocketMock);5758    void (async () => {59      try {60        const selector = decode(await receiveClient()) as Record<string, unknown>;61        resolve(selector);62        const session = await noiseKKResponder(63          responder.secretKey,64          responder.publicKey,65          initiator.publicKey,66          sendServer,67          receiveClient,68        );69        const theirIdentity = decode(session.decrypt(await receiveClient()));70        if (theirIdentity.long_term_public_key_hex !== hex(initiator.publicKey)) {71          throw new Error("initiator identity was not bound to KK key");72        }73        await sendServer(session.encrypt(encode({74          type: "PAIR_IDENTITY",75          device_id: "phone",76          device_name: "Pixel",77          device_type: "android",78          long_term_public_key_hex: peerIdentityKeyHex ?? hex(responder.publicKey),79        })));80      } catch (error) {81        reject(error);82      }83    })();84  });8586  const identity: SyncPeerIdentity = {87    deviceId: "browser",88    deviceName: "Firefox",89    deviceType: "extension",90    longTermPublicKeyHex: hex(initiator.publicKey),91    longTermPrivateKey: initiator.secretKey,92  };93  return { identity, responder, selectorSeen };94}9596afterEach(() => vi.unstubAllGlobals());9798describe("KK WebSocket reconnect transport", () => {99  it("selects paired state then completes KK with both keys pinned", async () => {100    const peer = installPeer();101    const session = await openSyncSession(102      { ip: "127.0.0.1", port: 49142 },103      peer.identity,104      hex(peer.responder.publicKey),105      RENDEZVOUS_ID,106    );107108    await expect(peer.selectorSeen).resolves.toEqual({109      type: "PEACH_RECONNECT",110      rendezvous_id: RENDEZVOUS_ID,111    });112    expect(hex(session.remoteStaticPublicKey)).toBe(hex(peer.responder.publicKey));113    await session.close();114  });115116  it("rejects a post-KK identity that claims a different responder key", async () => {117    const peer = installPeer("ff".repeat(32));118    await expect(119      openSyncSession(120        { ip: "127.0.0.1", port: 49142 },121        peer.identity,122        hex(peer.responder.publicKey),123        RENDEZVOUS_ID,124      ),125    ).rejects.toThrow(/identity does not match authenticated KK key/);126  });127});