vault-adopt.test.ts

10.0 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 "./vault-adopt";11import type { PairingTransport } from "./pairing-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      confirmSas: vi.fn(async () => true),61      createVaultFromRootKey: vi.fn(async () => ({ vaultId })),62      deriveLocalIdentity: vi.fn(async () => identity("receiver")),63      upsertPairedDevice: receiverUpsert,64      applyLicense: vi.fn(),65      onError: vi.fn(),66    };67    const sourceDeps: AdoptionSourceDeps = {68      sourceIdentity: identity("source"),69      rootKeyHex: vi.fn(async () => rootKeyHex),70      vaultId: vi.fn(async () => vaultId),71      license: vi.fn(async () => "PPK-test"),72      onConsent: vi.fn(async () => true),73      upsertPairedDevice: sourceUpsert,74    };7576    await Promise.all([77      requestAdoption(receiverChannel, receiverDeps),78      (async () => {79        const request = JSON.parse(new TextDecoder().decode(await sourceChannel.receive())) as AdoptRequest;80        await grantAdoption(sourceChannel, request, sourceDeps);81      })(),82    ]);8384    expect(receiverDeps.createVaultFromRootKey).toHaveBeenCalledWith(rootKeyHex);85    expect(receiverUpsert).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "source-id" }));86    expect(sourceUpsert).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "receiver-id" }));87    expect(receiverDeps.applyLicense).toHaveBeenCalledWith("PPK-test");88  });8990  it("T-A11: SAS reject aborts before ADOPT_GRANT", async () => {91    const [receiverChannel, sourceChannel] = memoryPair();92    const rootKeyHex = vi.fn(async () => "11".repeat(32));93    const createVaultFromRootKey = vi.fn();94    const receiver = requestAdoption(receiverChannel, {95      deviceName: "Receiver",96      generateDeviceId: () => "receiver-ephemeral",97      confirmSas: vi.fn(async () => false),98      createVaultFromRootKey,99      deriveLocalIdentity: vi.fn(),100      upsertPairedDevice: vi.fn(),101      onError: vi.fn(),102    });103    const request = JSON.parse(104      new TextDecoder().decode(await sourceChannel.receive()),105    ) as AdoptRequest;106    const source = grantAdoption(sourceChannel, request, {107      sourceIdentity: identity("source"),108      rootKeyHex,109      vaultId: vi.fn(),110      onConsent: vi.fn(async () => true),111      upsertPairedDevice: vi.fn(),112    });113114    await expect(receiver).resolves.toBeNull();115    await expect(source).resolves.toBeNull();116    expect(rootKeyHex).not.toHaveBeenCalled();117    expect(createVaultFromRootKey).not.toHaveBeenCalled();118  });119120  it("T-A12: consent reject aborts before ADOPT_GRANT and receiver remains vault-less", async () => {121    const [receiverChannel, sourceChannel] = memoryPair();122    const createVaultFromRootKey = vi.fn();123    const errors: string[] = [];124    const receiver = requestAdoption(receiverChannel, {125      deviceName: "Receiver",126      generateDeviceId: () => "receiver-ephemeral",127      confirmSas: vi.fn(async () => true),128      createVaultFromRootKey,129      deriveLocalIdentity: vi.fn(),130      upsertPairedDevice: vi.fn(),131      onError: (err) => errors.push(err),132    });133    const request = JSON.parse(new TextDecoder().decode(await sourceChannel.receive())) as AdoptRequest;134    await grantAdoption(sourceChannel, request, {135      sourceIdentity: identity("source"),136      rootKeyHex: vi.fn(),137      vaultId: vi.fn(),138      onConsent: vi.fn(async () => false),139      upsertPairedDevice: vi.fn(),140    });141    await Promise.race([receiver, Promise.resolve()]);142143    expect(createVaultFromRootKey).not.toHaveBeenCalled();144    expect(errors).toEqual([]);145  });146147  it("T-A13: invalid root_key_hex aborts gracefully", async () => {148    const [receiverChannel, sourceChannel] = memoryPair();149    const onError = vi.fn();150    const createVaultFromRootKey = vi.fn();151    const receiver = requestAdoption(receiverChannel, {152      deviceName: "Receiver",153      generateDeviceId: () => "receiver-ephemeral",154      confirmSas: vi.fn(async () => true),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: "bad",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    } as never);167    await receiver;168169    expect(onError).toHaveBeenCalledWith("ADOPT_UNEXPECTED_FRAME");170    expect(createVaultFromRootKey).not.toHaveBeenCalled();171  });172173  it("T-A14: vault_id mismatch aborts before vault creation", async () => {174    const [receiverChannel, sourceChannel] = memoryPair();175    const onError = vi.fn();176    const createVaultFromRootKey = vi.fn();177    const receiver = requestAdoption(receiverChannel, {178      deviceName: "Receiver",179      generateDeviceId: () => "receiver-ephemeral",180      confirmSas: vi.fn(async () => true),181      createVaultFromRootKey,182      deriveLocalIdentity: vi.fn(),183      upsertPairedDevice: vi.fn(),184      onError,185    });186    await sourceChannel.receive();187    await sendFrame(sourceChannel, {188      type: "ADOPT_GRANT",189      root_key_hex: "22".repeat(32),190      vault_id: "00".repeat(32),191      source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },192    });193    await receiver;194195    expect(onError).toHaveBeenCalledWith("ADOPT_VAULT_ID_MISMATCH");196    expect(createVaultFromRootKey).not.toHaveBeenCalled();197  });198199  it("T-A15: source does not persist receiver until ADOPT_IDENTITY arrives", async () => {200    const [receiverChannel, sourceChannel] = memoryPair();201    const upsert = vi.fn();202    const promise = grantAdoption(sourceChannel, {203      type: "ADOPT_REQUEST",204      device_id: "receiver-ephemeral",205      device_name: "Receiver",206      device_type: "extension",207    }, {208      sourceIdentity: identity("source"),209      rootKeyHex: vi.fn(async () => "33".repeat(32)),210      vaultId: vi.fn(async () => await computeVaultId(Uint8Array.from(Buffer.from("33".repeat(32), "hex")))),211      onConsent: vi.fn(async () => true),212      upsertPairedDevice: upsert,213    });214    await sendFrame(receiverChannel, {215      type: "ADOPT_CONFIRM",216      confirmed: true,217    });218219    await Promise.race([promise, Promise.resolve()]);220    expect(upsert).not.toHaveBeenCalled();221  });222223  it("T-A31: does not apply a license if vault creation fails", async () => {224    const [receiverChannel, sourceChannel] = memoryPair();225    const rootKeyHex = "44".repeat(32);226    const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex")));227    const applyLicense = vi.fn();228    const receiver = requestAdoption(receiverChannel, {229      deviceName: "Receiver",230      generateDeviceId: () => "receiver-ephemeral",231      confirmSas: vi.fn(async () => true),232      createVaultFromRootKey: vi.fn(async () => {233        throw new Error("commit_failed");234      }),235      deriveLocalIdentity: vi.fn(),236      upsertPairedDevice: vi.fn(),237      applyLicense,238      onError: vi.fn(),239    });240    await sourceChannel.receive();241    await sendFrame(sourceChannel, {242      type: "ADOPT_GRANT",243      root_key_hex: rootKeyHex,244      vault_id: vaultId,245      license: "PPK-test",246      source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },247    });248249    await expect(receiver).rejects.toThrow("commit_failed");250    expect(applyLicense).not.toHaveBeenCalled();251  });252253  it("T-A32: never creates a vault when the grant vault_id mismatches the root key", async () => {254    const [receiverChannel, sourceChannel] = memoryPair();255    const createVaultFromRootKey = vi.fn();256    const receiver = requestAdoption(receiverChannel, {257      deviceName: "Receiver",258      generateDeviceId: () => "receiver-ephemeral",259      confirmSas: vi.fn(async () => true),260      createVaultFromRootKey,261      deriveLocalIdentity: vi.fn(),262      upsertPairedDevice: vi.fn(),263      applyLicense: vi.fn(),264      onError: vi.fn(),265    });266    await sourceChannel.receive();267    await sendFrame(sourceChannel, {268      type: "ADOPT_GRANT",269      root_key_hex: "55".repeat(32),270      vault_id: "99".repeat(32),271      license: "PPK-test",272      source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },273    });274275    await receiver;276    expect(createVaultFromRootKey).not.toHaveBeenCalled();277  });278});