vault-adopt.test.ts

10.2 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 pairState = {58      rendezvousSecretHex: "42".repeat(32),59      relayUrl: "wss://relay.example.test",60    };61    const receiverDeps: AdoptionReceiverDeps = {62      deviceName: "Receiver",63      generateDeviceId: () => "receiver-ephemeral",64      confirmSas: vi.fn(async () => true),65      createVaultFromRootKey: vi.fn(async () => ({ vaultId })),66      deriveLocalIdentity: vi.fn(async () => identity("receiver")),67      upsertPairedDevice: receiverUpsert,68      applyLicense: vi.fn(),69      pairState,70      onError: vi.fn(),71    };72    const sourceDeps: AdoptionSourceDeps = {73      sourceIdentity: identity("source"),74      rootKeyHex: vi.fn(async () => rootKeyHex),75      vaultId: vi.fn(async () => vaultId),76      license: vi.fn(async () => "PPK-test"),77      onConsent: vi.fn(async () => true),78      upsertPairedDevice: sourceUpsert,79      pairState,80    };8182    await Promise.all([83      requestAdoption(receiverChannel, receiverDeps),84      (async () => {85        const request = JSON.parse(new TextDecoder().decode(await sourceChannel.receive())) as AdoptRequest;86        await grantAdoption(sourceChannel, request, sourceDeps);87      })(),88    ]);8990    expect(receiverDeps.createVaultFromRootKey).toHaveBeenCalledWith(rootKeyHex);91    expect(receiverUpsert).toHaveBeenCalledWith(expect.objectContaining({92      deviceId: "source-id",93      ...pairState,94    }));95    expect(sourceUpsert).toHaveBeenCalledWith(expect.objectContaining({96      deviceId: "receiver-id",97      ...pairState,98    }));99    expect(receiverDeps.applyLicense).toHaveBeenCalledWith("PPK-test");100  });101102  it("T-A11: SAS reject aborts before ADOPT_GRANT", async () => {103    const [receiverChannel, sourceChannel] = memoryPair();104    const rootKeyHex = vi.fn(async () => "11".repeat(32));105    const createVaultFromRootKey = vi.fn();106    const receiver = requestAdoption(receiverChannel, {107      deviceName: "Receiver",108      generateDeviceId: () => "receiver-ephemeral",109      confirmSas: vi.fn(async () => false),110      createVaultFromRootKey,111      deriveLocalIdentity: vi.fn(),112      upsertPairedDevice: vi.fn(),113      onError: vi.fn(),114    });115    const request = JSON.parse(116      new TextDecoder().decode(await sourceChannel.receive()),117    ) as AdoptRequest;118    const source = grantAdoption(sourceChannel, request, {119      sourceIdentity: identity("source"),120      rootKeyHex,121      vaultId: vi.fn(),122      onConsent: vi.fn(async () => true),123      upsertPairedDevice: vi.fn(),124    });125126    await expect(receiver).resolves.toBeNull();127    await expect(source).resolves.toBeNull();128    expect(rootKeyHex).not.toHaveBeenCalled();129    expect(createVaultFromRootKey).not.toHaveBeenCalled();130  });131132  it("T-A12: consent reject aborts before ADOPT_GRANT and receiver remains vault-less", async () => {133    const [receiverChannel, sourceChannel] = memoryPair();134    const createVaultFromRootKey = vi.fn();135    const errors: string[] = [];136    const receiver = requestAdoption(receiverChannel, {137      deviceName: "Receiver",138      generateDeviceId: () => "receiver-ephemeral",139      confirmSas: vi.fn(async () => true),140      createVaultFromRootKey,141      deriveLocalIdentity: vi.fn(),142      upsertPairedDevice: vi.fn(),143      onError: (err) => errors.push(err),144    });145    const request = JSON.parse(new TextDecoder().decode(await sourceChannel.receive())) as AdoptRequest;146    await grantAdoption(sourceChannel, request, {147      sourceIdentity: identity("source"),148      rootKeyHex: vi.fn(),149      vaultId: vi.fn(),150      onConsent: vi.fn(async () => false),151      upsertPairedDevice: vi.fn(),152    });153    await Promise.race([receiver, Promise.resolve()]);154155    expect(createVaultFromRootKey).not.toHaveBeenCalled();156    expect(errors).toEqual([]);157  });158159  it("T-A13: invalid root_key_hex aborts gracefully", async () => {160    const [receiverChannel, sourceChannel] = memoryPair();161    const onError = vi.fn();162    const createVaultFromRootKey = vi.fn();163    const receiver = requestAdoption(receiverChannel, {164      deviceName: "Receiver",165      generateDeviceId: () => "receiver-ephemeral",166      confirmSas: vi.fn(async () => true),167      createVaultFromRootKey,168      deriveLocalIdentity: vi.fn(),169      upsertPairedDevice: vi.fn(),170      onError,171    });172    await sourceChannel.receive();173    await sendFrame(sourceChannel, {174      type: "ADOPT_GRANT",175      root_key_hex: "bad",176      vault_id: "00".repeat(32),177      source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },178    } as never);179    await receiver;180181    expect(onError).toHaveBeenCalledWith("ADOPT_UNEXPECTED_FRAME");182    expect(createVaultFromRootKey).not.toHaveBeenCalled();183  });184185  it("T-A14: vault_id mismatch aborts before vault creation", async () => {186    const [receiverChannel, sourceChannel] = memoryPair();187    const onError = vi.fn();188    const createVaultFromRootKey = vi.fn();189    const receiver = requestAdoption(receiverChannel, {190      deviceName: "Receiver",191      generateDeviceId: () => "receiver-ephemeral",192      confirmSas: vi.fn(async () => true),193      createVaultFromRootKey,194      deriveLocalIdentity: vi.fn(),195      upsertPairedDevice: vi.fn(),196      onError,197    });198    await sourceChannel.receive();199    await sendFrame(sourceChannel, {200      type: "ADOPT_GRANT",201      root_key_hex: "22".repeat(32),202      vault_id: "00".repeat(32),203      source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },204    });205    await receiver;206207    expect(onError).toHaveBeenCalledWith("ADOPT_VAULT_ID_MISMATCH");208    expect(createVaultFromRootKey).not.toHaveBeenCalled();209  });210211  it("T-A15: source does not persist receiver until ADOPT_IDENTITY arrives", async () => {212    const [receiverChannel, sourceChannel] = memoryPair();213    const upsert = vi.fn();214    const promise = grantAdoption(sourceChannel, {215      type: "ADOPT_REQUEST",216      device_id: "receiver-ephemeral",217      device_name: "Receiver",218      device_type: "extension",219    }, {220      sourceIdentity: identity("source"),221      rootKeyHex: vi.fn(async () => "33".repeat(32)),222      vaultId: vi.fn(async () => await computeVaultId(Uint8Array.from(Buffer.from("33".repeat(32), "hex")))),223      onConsent: vi.fn(async () => true),224      upsertPairedDevice: upsert,225    });226    await sendFrame(receiverChannel, {227      type: "ADOPT_CONFIRM",228      confirmed: true,229    });230231    await Promise.race([promise, Promise.resolve()]);232    expect(upsert).not.toHaveBeenCalled();233  });234235  it("T-A31: does not apply a license if vault creation fails", async () => {236    const [receiverChannel, sourceChannel] = memoryPair();237    const rootKeyHex = "44".repeat(32);238    const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex")));239    const applyLicense = vi.fn();240    const receiver = requestAdoption(receiverChannel, {241      deviceName: "Receiver",242      generateDeviceId: () => "receiver-ephemeral",243      confirmSas: vi.fn(async () => true),244      createVaultFromRootKey: vi.fn(async () => {245        throw new Error("commit_failed");246      }),247      deriveLocalIdentity: vi.fn(),248      upsertPairedDevice: vi.fn(),249      applyLicense,250      onError: vi.fn(),251    });252    await sourceChannel.receive();253    await sendFrame(sourceChannel, {254      type: "ADOPT_GRANT",255      root_key_hex: rootKeyHex,256      vault_id: vaultId,257      license: "PPK-test",258      source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },259    });260261    await expect(receiver).rejects.toThrow("commit_failed");262    expect(applyLicense).not.toHaveBeenCalled();263  });264265  it("T-A32: never creates a vault when the grant vault_id mismatches the root key", async () => {266    const [receiverChannel, sourceChannel] = memoryPair();267    const createVaultFromRootKey = vi.fn();268    const receiver = requestAdoption(receiverChannel, {269      deviceName: "Receiver",270      generateDeviceId: () => "receiver-ephemeral",271      confirmSas: vi.fn(async () => true),272      createVaultFromRootKey,273      deriveLocalIdentity: vi.fn(),274      upsertPairedDevice: vi.fn(),275      applyLicense: vi.fn(),276      onError: vi.fn(),277    });278    await sourceChannel.receive();279    await sendFrame(sourceChannel, {280      type: "ADOPT_GRANT",281      root_key_hex: "55".repeat(32),282      vault_id: "99".repeat(32),283      license: "PPK-test",284      source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },285    });286287    await receiver;288    expect(createVaultFromRootKey).not.toHaveBeenCalled();289  });290});