import { describe, expect, it, vi } from "vitest"; import { computeVaultId } from "./sync-identity"; import { grantAdoption, requestAdoption, sendFrame, type AdoptRequest, type AdoptionReceiverDeps, type AdoptionSourceDeps, } from "./vault-adopt"; import type { PairingTransport } from "./pairing-handshake"; function memoryPair(): [PairingTransport, PairingTransport] { const a: Uint8Array[] = []; const b: Uint8Array[] = []; const ar: Array<(v: Uint8Array) => void> = []; const br: Array<(v: Uint8Array) => void> = []; function make( inbox: Uint8Array[], outbox: Uint8Array[], readers: Array<(v: Uint8Array) => void>, outReaders: Array<(v: Uint8Array) => void>, ): PairingTransport { return { async send(msg) { const reader = outReaders.shift(); if (reader) reader(msg); else outbox.push(msg); }, receive() { const msg = inbox.shift(); if (msg) return Promise.resolve(msg); return new Promise((resolve) => readers.push(resolve)); }, }; } return [make(a, b, ar, br), make(b, a, br, ar)]; } function identity(name: string) { return { deviceId: `${name}-id`, deviceName: name, deviceType: "extension" as const, longTermPublicKeyHex: "ab".repeat(32), signingPublicKeyHex: "cd".repeat(32), }; } describe("vault adoption sub-protocol", () => { it("T-A10: completes ADOPT_REQUEST/ADOPT_GRANT/ADOPT_IDENTITY KAT", async () => { const [receiverChannel, sourceChannel] = memoryPair(); const rootKeyHex = "11".repeat(32); const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex"))); const sourceUpsert = vi.fn(); const receiverUpsert = vi.fn(); const receiverDeps: AdoptionReceiverDeps = { deviceName: "Receiver", generateDeviceId: () => "receiver-ephemeral", confirmSas: vi.fn(async () => true), createVaultFromRootKey: vi.fn(async () => ({ vaultId })), deriveLocalIdentity: vi.fn(async () => identity("receiver")), upsertPairedDevice: receiverUpsert, applyLicense: vi.fn(), onError: vi.fn(), }; const sourceDeps: AdoptionSourceDeps = { sourceIdentity: identity("source"), rootKeyHex: vi.fn(async () => rootKeyHex), vaultId: vi.fn(async () => vaultId), license: vi.fn(async () => "PPK-test"), onConsent: vi.fn(async () => true), upsertPairedDevice: sourceUpsert, }; await Promise.all([ requestAdoption(receiverChannel, receiverDeps), (async () => { const request = JSON.parse(new TextDecoder().decode(await sourceChannel.receive())) as AdoptRequest; await grantAdoption(sourceChannel, request, sourceDeps); })(), ]); expect(receiverDeps.createVaultFromRootKey).toHaveBeenCalledWith(rootKeyHex); expect(receiverUpsert).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "source-id" })); expect(sourceUpsert).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "receiver-id" })); expect(receiverDeps.applyLicense).toHaveBeenCalledWith("PPK-test"); }); it("T-A11: SAS reject aborts before ADOPT_GRANT", async () => { const [receiverChannel, sourceChannel] = memoryPair(); const rootKeyHex = vi.fn(async () => "11".repeat(32)); const createVaultFromRootKey = vi.fn(); const receiver = requestAdoption(receiverChannel, { deviceName: "Receiver", generateDeviceId: () => "receiver-ephemeral", confirmSas: vi.fn(async () => false), createVaultFromRootKey, deriveLocalIdentity: vi.fn(), upsertPairedDevice: vi.fn(), onError: vi.fn(), }); const request = JSON.parse( new TextDecoder().decode(await sourceChannel.receive()), ) as AdoptRequest; const source = grantAdoption(sourceChannel, request, { sourceIdentity: identity("source"), rootKeyHex, vaultId: vi.fn(), onConsent: vi.fn(async () => true), upsertPairedDevice: vi.fn(), }); await expect(receiver).resolves.toBeNull(); await expect(source).resolves.toBeNull(); expect(rootKeyHex).not.toHaveBeenCalled(); expect(createVaultFromRootKey).not.toHaveBeenCalled(); }); it("T-A12: consent reject aborts before ADOPT_GRANT and receiver remains vault-less", async () => { const [receiverChannel, sourceChannel] = memoryPair(); const createVaultFromRootKey = vi.fn(); const errors: string[] = []; const receiver = requestAdoption(receiverChannel, { deviceName: "Receiver", generateDeviceId: () => "receiver-ephemeral", confirmSas: vi.fn(async () => true), createVaultFromRootKey, deriveLocalIdentity: vi.fn(), upsertPairedDevice: vi.fn(), onError: (err) => errors.push(err), }); const request = JSON.parse(new TextDecoder().decode(await sourceChannel.receive())) as AdoptRequest; await grantAdoption(sourceChannel, request, { sourceIdentity: identity("source"), rootKeyHex: vi.fn(), vaultId: vi.fn(), onConsent: vi.fn(async () => false), upsertPairedDevice: vi.fn(), }); await Promise.race([receiver, Promise.resolve()]); expect(createVaultFromRootKey).not.toHaveBeenCalled(); expect(errors).toEqual([]); }); it("T-A13: invalid root_key_hex aborts gracefully", async () => { const [receiverChannel, sourceChannel] = memoryPair(); const onError = vi.fn(); const createVaultFromRootKey = vi.fn(); const receiver = requestAdoption(receiverChannel, { deviceName: "Receiver", generateDeviceId: () => "receiver-ephemeral", confirmSas: vi.fn(async () => true), createVaultFromRootKey, deriveLocalIdentity: vi.fn(), upsertPairedDevice: vi.fn(), onError, }); await sourceChannel.receive(); await sendFrame(sourceChannel, { type: "ADOPT_GRANT", root_key_hex: "bad", vault_id: "00".repeat(32), source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) }, } as never); await receiver; expect(onError).toHaveBeenCalledWith("ADOPT_UNEXPECTED_FRAME"); expect(createVaultFromRootKey).not.toHaveBeenCalled(); }); it("T-A14: vault_id mismatch aborts before vault creation", async () => { const [receiverChannel, sourceChannel] = memoryPair(); const onError = vi.fn(); const createVaultFromRootKey = vi.fn(); const receiver = requestAdoption(receiverChannel, { deviceName: "Receiver", generateDeviceId: () => "receiver-ephemeral", confirmSas: vi.fn(async () => true), createVaultFromRootKey, deriveLocalIdentity: vi.fn(), upsertPairedDevice: vi.fn(), onError, }); await sourceChannel.receive(); await sendFrame(sourceChannel, { type: "ADOPT_GRANT", root_key_hex: "22".repeat(32), vault_id: "00".repeat(32), source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) }, }); await receiver; expect(onError).toHaveBeenCalledWith("ADOPT_VAULT_ID_MISMATCH"); expect(createVaultFromRootKey).not.toHaveBeenCalled(); }); it("T-A15: source does not persist receiver until ADOPT_IDENTITY arrives", async () => { const [receiverChannel, sourceChannel] = memoryPair(); const upsert = vi.fn(); const promise = grantAdoption(sourceChannel, { type: "ADOPT_REQUEST", device_id: "receiver-ephemeral", device_name: "Receiver", device_type: "extension", }, { sourceIdentity: identity("source"), rootKeyHex: vi.fn(async () => "33".repeat(32)), vaultId: vi.fn(async () => await computeVaultId(Uint8Array.from(Buffer.from("33".repeat(32), "hex")))), onConsent: vi.fn(async () => true), upsertPairedDevice: upsert, }); await sendFrame(receiverChannel, { type: "ADOPT_CONFIRM", confirmed: true, }); await Promise.race([promise, Promise.resolve()]); expect(upsert).not.toHaveBeenCalled(); }); it("T-A31: does not apply a license if vault creation fails", async () => { const [receiverChannel, sourceChannel] = memoryPair(); const rootKeyHex = "44".repeat(32); const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex"))); const applyLicense = vi.fn(); const receiver = requestAdoption(receiverChannel, { deviceName: "Receiver", generateDeviceId: () => "receiver-ephemeral", confirmSas: vi.fn(async () => true), createVaultFromRootKey: vi.fn(async () => { throw new Error("commit_failed"); }), deriveLocalIdentity: vi.fn(), upsertPairedDevice: vi.fn(), applyLicense, onError: vi.fn(), }); await sourceChannel.receive(); await sendFrame(sourceChannel, { type: "ADOPT_GRANT", root_key_hex: rootKeyHex, vault_id: vaultId, license: "PPK-test", source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) }, }); await expect(receiver).rejects.toThrow("commit_failed"); expect(applyLicense).not.toHaveBeenCalled(); }); it("T-A32: never creates a vault when the grant vault_id mismatches the root key", async () => { const [receiverChannel, sourceChannel] = memoryPair(); const createVaultFromRootKey = vi.fn(); const receiver = requestAdoption(receiverChannel, { deviceName: "Receiver", generateDeviceId: () => "receiver-ephemeral", confirmSas: vi.fn(async () => true), createVaultFromRootKey, deriveLocalIdentity: vi.fn(), upsertPairedDevice: vi.fn(), applyLicense: vi.fn(), onError: vi.fn(), }); await sourceChannel.receive(); await sendFrame(sourceChannel, { type: "ADOPT_GRANT", root_key_hex: "55".repeat(32), vault_id: "99".repeat(32), license: "PPK-test", source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) }, }); await receiver; expect(createVaultFromRootKey).not.toHaveBeenCalled(); }); });