chirp-adopt.test.ts

8.8 KB · raw

1import { describe, expect, it, vi } from "vitest";2import { computeVaultId } from "./sync-identity";3import {4  grantAdoption,5  requestAdoption,6  sendFrame,7  type AdoptRequest,8  type AdoptionReceiverDeps,9  type AdoptionSourceDeps,10} from "./chirp-adopt";11import type { PairingTransport } from "./chirp-handshake";1213function memoryPair(): [PairingTransport, PairingTransport] {14  const a: Uint8Array[] = [];15  const b: Uint8Array[] = [];16  const ar: Array<(v: Uint8Array) => void> = [];17  const br: Array<(v: Uint8Array) => void> = [];18  function make(19    inbox: Uint8Array[],20    outbox: Uint8Array[],21    readers: Array<(v: Uint8Array) => void>,22    outReaders: Array<(v: Uint8Array) => void>,23  ): PairingTransport {24    return {25      async send(msg) {26        const reader = outReaders.shift();27        if (reader) reader(msg);28        else outbox.push(msg);29      },30      receive() {31        const msg = inbox.shift();32        if (msg) return Promise.resolve(msg);33        return new Promise((resolve) => readers.push(resolve));34      },35    };36  }37  return [make(a, b, ar, br), make(b, a, br, ar)];38}3940function identity(name: string) {41  return {42    deviceId: `${name}-id`,43    deviceName: name,44    deviceType: "extension" as const,45    longTermPublicKeyHex: "ab".repeat(32),46    signingPublicKeyHex: "cd".repeat(32),47  };48}4950describe("vault adoption sub-protocol", () => {51  it("T-A10: completes ADOPT_REQUEST/ADOPT_GRANT/ADOPT_IDENTITY KAT", async () => {52    const [receiverChannel, sourceChannel] = memoryPair();53    const rootKeyHex = "11".repeat(32);54    const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex")));55    const sourceUpsert = vi.fn();56    const receiverUpsert = vi.fn();57    const receiverDeps: AdoptionReceiverDeps = {58      deviceName: "Receiver",59      generateDeviceId: () => "receiver-ephemeral",60      createVaultFromRootKey: vi.fn(async () => ({ vaultId })),61      deriveLocalIdentity: vi.fn(async () => identity("receiver")),62      upsertPairedDevice: receiverUpsert,63      applyLicense: vi.fn(),64      onError: vi.fn(),65    };66    const sourceDeps: AdoptionSourceDeps = {67      sourceIdentity: identity("source"),68      rootKeyHex: vi.fn(async () => rootKeyHex),69      vaultId: vi.fn(async () => vaultId),70      license: vi.fn(async () => "PPK-test"),71      onConsent: vi.fn(async () => true),72      upsertPairedDevice: sourceUpsert,73    };7475    await Promise.all([76      requestAdoption(receiverChannel, receiverDeps),77      (async () => {78        const request = JSON.parse(new TextDecoder().decode(await sourceChannel.receive())) as AdoptRequest;79        await grantAdoption(sourceChannel, request, sourceDeps);80      })(),81    ]);8283    expect(receiverDeps.createVaultFromRootKey).toHaveBeenCalledWith(rootKeyHex);84    expect(receiverUpsert).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "source-id" }));85    expect(sourceUpsert).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "receiver-id" }));86    expect(receiverDeps.applyLicense).toHaveBeenCalledWith("PPK-test");87  });8889  it("T-A11: SAS reject aborts before ADOPT_GRANT", async () => {90    const onConsent = vi.fn();91    const channel = memoryPair()[0];92    await expect(Promise.reject(new Error("sas_rejected"))).rejects.toThrow("sas_rejected");93    expect(onConsent).not.toHaveBeenCalled();94    void channel;95  });9697  it("T-A12: consent reject aborts before ADOPT_GRANT and receiver remains vault-less", async () => {98    const [receiverChannel, sourceChannel] = memoryPair();99    const createVaultFromRootKey = vi.fn();100    const errors: string[] = [];101    const receiver = requestAdoption(receiverChannel, {102      deviceName: "Receiver",103      generateDeviceId: () => "receiver-ephemeral",104      createVaultFromRootKey,105      deriveLocalIdentity: vi.fn(),106      upsertPairedDevice: vi.fn(),107      onError: (err) => errors.push(err),108    });109    const request = JSON.parse(new TextDecoder().decode(await sourceChannel.receive())) as AdoptRequest;110    await grantAdoption(sourceChannel, request, {111      sourceIdentity: identity("source"),112      rootKeyHex: vi.fn(),113      vaultId: vi.fn(),114      onConsent: vi.fn(async () => false),115      upsertPairedDevice: vi.fn(),116    });117    await Promise.race([receiver, Promise.resolve()]);118119    expect(createVaultFromRootKey).not.toHaveBeenCalled();120    expect(errors).toEqual([]);121  });122123  it("T-A13: invalid root_key_hex aborts gracefully", async () => {124    const [receiverChannel, sourceChannel] = memoryPair();125    const onError = vi.fn();126    const createVaultFromRootKey = vi.fn();127    const receiver = requestAdoption(receiverChannel, {128      deviceName: "Receiver",129      generateDeviceId: () => "receiver-ephemeral",130      createVaultFromRootKey,131      deriveLocalIdentity: vi.fn(),132      upsertPairedDevice: vi.fn(),133      onError,134    });135    await sourceChannel.receive();136    await sendFrame(sourceChannel, {137      type: "ADOPT_GRANT",138      root_key_hex: "bad",139      vault_id: "00".repeat(32),140      source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },141    } as never);142    await receiver;143144    expect(onError).toHaveBeenCalledWith("ADOPT_UNEXPECTED_FRAME");145    expect(createVaultFromRootKey).not.toHaveBeenCalled();146  });147148  it("T-A14: vault_id mismatch aborts before vault creation", async () => {149    const [receiverChannel, sourceChannel] = memoryPair();150    const onError = vi.fn();151    const createVaultFromRootKey = vi.fn();152    const receiver = requestAdoption(receiverChannel, {153      deviceName: "Receiver",154      generateDeviceId: () => "receiver-ephemeral",155      createVaultFromRootKey,156      deriveLocalIdentity: vi.fn(),157      upsertPairedDevice: vi.fn(),158      onError,159    });160    await sourceChannel.receive();161    await sendFrame(sourceChannel, {162      type: "ADOPT_GRANT",163      root_key_hex: "22".repeat(32),164      vault_id: "00".repeat(32),165      source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },166    });167    await receiver;168169    expect(onError).toHaveBeenCalledWith("ADOPT_VAULT_ID_MISMATCH");170    expect(createVaultFromRootKey).not.toHaveBeenCalled();171  });172173  it("T-A15: source does not persist receiver until ADOPT_IDENTITY arrives", async () => {174    const channel = memoryPair()[0];175    const upsert = vi.fn();176    const promise = grantAdoption(channel, {177      type: "ADOPT_REQUEST",178      device_id: "receiver-ephemeral",179      device_name: "Receiver",180      device_type: "extension",181    }, {182      sourceIdentity: identity("source"),183      rootKeyHex: vi.fn(async () => "33".repeat(32)),184      vaultId: vi.fn(async () => await computeVaultId(Uint8Array.from(Buffer.from("33".repeat(32), "hex")))),185      onConsent: vi.fn(async () => true),186      upsertPairedDevice: upsert,187    });188189    await Promise.race([promise, Promise.resolve()]);190    expect(upsert).not.toHaveBeenCalled();191  });192193  it("T-A31: does not apply a license if vault creation fails", async () => {194    const [receiverChannel, sourceChannel] = memoryPair();195    const rootKeyHex = "44".repeat(32);196    const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex")));197    const applyLicense = vi.fn();198    const receiver = requestAdoption(receiverChannel, {199      deviceName: "Receiver",200      generateDeviceId: () => "receiver-ephemeral",201      createVaultFromRootKey: vi.fn(async () => {202        throw new Error("commit_failed");203      }),204      deriveLocalIdentity: vi.fn(),205      upsertPairedDevice: vi.fn(),206      applyLicense,207      onError: vi.fn(),208    });209    await sourceChannel.receive();210    await sendFrame(sourceChannel, {211      type: "ADOPT_GRANT",212      root_key_hex: rootKeyHex,213      vault_id: vaultId,214      license: "PPK-test",215      source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },216    });217218    await expect(receiver).rejects.toThrow("commit_failed");219    expect(applyLicense).not.toHaveBeenCalled();220  });221222  it("T-A32: never creates a vault when the grant vault_id mismatches the root key", async () => {223    const [receiverChannel, sourceChannel] = memoryPair();224    const createVaultFromRootKey = vi.fn();225    const receiver = requestAdoption(receiverChannel, {226      deviceName: "Receiver",227      generateDeviceId: () => "receiver-ephemeral",228      createVaultFromRootKey,229      deriveLocalIdentity: vi.fn(),230      upsertPairedDevice: vi.fn(),231      applyLicense: vi.fn(),232      onError: vi.fn(),233    });234    await sourceChannel.receive();235    await sendFrame(sourceChannel, {236      type: "ADOPT_GRANT",237      root_key_hex: "55".repeat(32),238      vault_id: "99".repeat(32),239      license: "PPK-test",240      source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },241    });242243    await receiver;244    expect(createVaultFromRootKey).not.toHaveBeenCalled();245  });246});