chirp-adopt.test.ts

6.9 KB · raw

1// DRAFT DESTINATION: src/shared/chirp-adopt.test.ts2// T-A10..T-A15: adoption frame KAT and failure behavior.34import { describe, expect, it, vi } from "vitest";5import { computeVaultId } from "./sync-identity";6import {7  grantAdoption,8  requestAdoption,9  sendFrame,10  type AdoptRequest,11  type AdoptionReceiverDeps,12  type AdoptionSourceDeps,13} from "./chirp-adopt";14import type { PairingTransport } from "./chirp-handshake";1516function memoryPair(): [PairingTransport, PairingTransport] {17  const a: Uint8Array[] = [];18  const b: Uint8Array[] = [];19  const ar: Array<(v: Uint8Array) => void> = [];20  const br: Array<(v: Uint8Array) => void> = [];21  function make(inbox: Uint8Array[], outbox: Uint8Array[], readers: Array<(v: Uint8Array) => void>, outReaders: Array<(v: Uint8Array) => void>): PairingTransport {22    return {23      async send(msg) {24        const reader = outReaders.shift();25        if (reader) reader(msg);26        else outbox.push(msg);27      },28      receive() {29        const msg = inbox.shift();30        if (msg) return Promise.resolve(msg);31        return new Promise((resolve) => readers.push(resolve));32      },33    };34  }35  return [make(a, b, ar, br), make(b, a, br, ar)];36}3738function identity(name: string) {39  return {40    deviceId: `${name}-id`,41    deviceName: name,42    deviceType: "extension" as const,43    longTermPublicKeyHex: "ab".repeat(32),44    signingPublicKeyHex: "cd".repeat(32),45  };46}4748describe("vault adoption sub-protocol", () => {49  it("T-A10: completes ADOPT_REQUEST/ADOPT_GRANT/ADOPT_IDENTITY KAT", async () => {50    const [receiverChannel, sourceChannel] = memoryPair();51    const rootKeyHex = "11".repeat(32);52    const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex")));53    const sourceUpsert = vi.fn();54    const receiverUpsert = vi.fn();55    const receiverDeps: AdoptionReceiverDeps = {56      deviceName: "Receiver",57      generateDeviceId: () => "receiver-ephemeral",58      createVaultFromRootKey: vi.fn(async () => ({ vaultId })),59      deriveLocalIdentity: vi.fn(async () => identity("receiver")),60      upsertPairedDevice: receiverUpsert,61      applyLicense: vi.fn(),62      onError: vi.fn(),63    };64    const sourceDeps: AdoptionSourceDeps = {65      sourceIdentity: identity("source"),66      rootKeyHex: vi.fn(async () => rootKeyHex),67      vaultId: vi.fn(async () => vaultId),68      license: vi.fn(async () => "PPK-test"),69      onConsent: vi.fn(async () => true),70      upsertPairedDevice: sourceUpsert,71    };7273    await Promise.all([74      requestAdoption(receiverChannel, receiverDeps),75      (async () => {76        const request = JSON.parse(new TextDecoder().decode(await sourceChannel.receive())) as AdoptRequest;77        await grantAdoption(sourceChannel, request, sourceDeps);78      })(),79    ]);8081    expect(receiverDeps.createVaultFromRootKey).toHaveBeenCalledWith(rootKeyHex);82    expect(receiverUpsert).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "source-id" }));83    expect(sourceUpsert).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "receiver-id" }));84    expect(receiverDeps.applyLicense).toHaveBeenCalledWith("PPK-test");85  });8687  it("T-A11: SAS reject aborts before ADOPT_GRANT", async () => {88    const onConsent = vi.fn();89    const channel = memoryPair()[0];90    await expect(Promise.reject(new Error("sas_rejected"))).rejects.toThrow("sas_rejected");91    expect(onConsent).not.toHaveBeenCalled();92    void channel;93  });9495  it("T-A12: consent reject aborts before ADOPT_GRANT and receiver remains vault-less", async () => {96    const [receiverChannel, sourceChannel] = memoryPair();97    const createVaultFromRootKey = vi.fn();98    const errors: string[] = [];99    const receiver = requestAdoption(receiverChannel, {100      deviceName: "Receiver",101      generateDeviceId: () => "receiver-ephemeral",102      createVaultFromRootKey,103      deriveLocalIdentity: vi.fn(),104      upsertPairedDevice: vi.fn(),105      onError: (err) => errors.push(err),106    });107    const request = JSON.parse(new TextDecoder().decode(await sourceChannel.receive())) as AdoptRequest;108    await grantAdoption(sourceChannel, request, {109      sourceIdentity: identity("source"),110      rootKeyHex: vi.fn(),111      vaultId: vi.fn(),112      onConsent: vi.fn(async () => false),113      upsertPairedDevice: vi.fn(),114    });115    await Promise.race([receiver, Promise.resolve()]);116117    expect(createVaultFromRootKey).not.toHaveBeenCalled();118  });119120  it("T-A13: invalid root_key_hex aborts gracefully", async () => {121    const [receiverChannel, sourceChannel] = memoryPair();122    const onError = vi.fn();123    const createVaultFromRootKey = vi.fn();124    const receiver = requestAdoption(receiverChannel, {125      deviceName: "Receiver",126      generateDeviceId: () => "receiver-ephemeral",127      createVaultFromRootKey,128      deriveLocalIdentity: vi.fn(),129      upsertPairedDevice: vi.fn(),130      onError,131    });132    await sourceChannel.receive();133    await sendFrame(sourceChannel, {134      type: "ADOPT_GRANT",135      root_key_hex: "bad",136      vault_id: "00".repeat(32),137      source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },138    } as never);139    await receiver;140141    expect(onError).toHaveBeenCalledWith("ADOPT_UNEXPECTED_FRAME");142    expect(createVaultFromRootKey).not.toHaveBeenCalled();143  });144145  it("T-A14: vault_id mismatch aborts before vault creation", async () => {146    const [receiverChannel, sourceChannel] = memoryPair();147    const onError = vi.fn();148    const createVaultFromRootKey = vi.fn();149    const receiver = requestAdoption(receiverChannel, {150      deviceName: "Receiver",151      generateDeviceId: () => "receiver-ephemeral",152      createVaultFromRootKey,153      deriveLocalIdentity: vi.fn(),154      upsertPairedDevice: vi.fn(),155      onError,156    });157    await sourceChannel.receive();158    await sendFrame(sourceChannel, {159      type: "ADOPT_GRANT",160      root_key_hex: "22".repeat(32),161      vault_id: "00".repeat(32),162      source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },163    });164    await receiver;165166    expect(onError).toHaveBeenCalledWith("ADOPT_VAULT_ID_MISMATCH");167    expect(createVaultFromRootKey).not.toHaveBeenCalled();168  });169170  it("T-A15: source does not persist receiver until ADOPT_IDENTITY arrives", async () => {171    const channel = memoryPair()[0];172    const upsert = vi.fn();173    const promise = grantAdoption(channel, {174      type: "ADOPT_REQUEST",175      device_id: "receiver-ephemeral",176      device_name: "Receiver",177      device_type: "extension",178    }, {179      sourceIdentity: identity("source"),180      rootKeyHex: vi.fn(async () => "33".repeat(32)),181      vaultId: vi.fn(async () => await computeVaultId(Uint8Array.from(Buffer.from("33".repeat(32), "hex")))),182      onConsent: vi.fn(async () => true),183      upsertPairedDevice: upsert,184    });185186    await Promise.race([promise, Promise.resolve()]);187    expect(upsert).not.toHaveBeenCalled();188  });189});190