/** * pairing-completed-gate.test.ts — M13/SEC-005 * * PAIRING_COMPLETED must verify the publicKeyHex came from a real Noise NK * handshake before calling addPairedDevice. Forged messages with an * unverified key must be rejected. */ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { Message, MessageResponse } from "../shared/types"; type StorageShape = Record; type MessageListener = ( msg: unknown, sender: chrome.runtime.MessageSender, sendResponse: (response: MessageResponse) => void ) => boolean; describe("PAIRING_COMPLETED gate (M13/SEC-005)", () => { let storage: StorageShape; let listener: MessageListener | undefined; beforeEach(async () => { vi.resetModules(); vi.useRealTimers(); storage = { peach_settings: { breachMonitoring: false } }; listener = undefined; vi.stubGlobal( "fetch", vi.fn(async () => ({ ok: true, status: 200, text: async () => "" })), ); vi.stubGlobal("__PEACH_E2E_PASSKEYS__", false); const chromeMock = { runtime: { id: "peach-extension", onMessage: { addListener: vi.fn((fn: MessageListener) => { listener = fn; }), }, sendMessage: vi.fn(async () => ({ ok: true })), }, storage: { local: { get: vi.fn(async (key?: string | string[]) => { if (!key) return storage; if (typeof key === "string") return { [key]: storage[key] }; return key.reduce>((acc, k) => { acc[k] = storage[k]; return acc; }, {}); }), set: vi.fn(async (next: Record) => { Object.assign(storage, next); }), remove: vi.fn(async (keys: string | string[]) => { const list = Array.isArray(keys) ? keys : [keys]; for (const k of list) delete storage[k]; }), }, session: { get: vi.fn(async () => ({})), set: vi.fn(async () => {}), remove: vi.fn(async () => {}), }, }, alarms: { clear: vi.fn(), create: vi.fn(), onAlarm: { addListener: vi.fn() }, }, idle: { setDetectionInterval: vi.fn(), onStateChanged: { addListener: vi.fn() }, }, scripting: { executeScript: vi.fn(async () => []) }, } as unknown as typeof chrome; (globalThis as { chrome?: typeof chrome }).chrome = chromeMock; const messages = await import("./messages"); messages.setupMessageHandler(); }); async function dispatch(message: Message): Promise { if (!listener) throw new Error("listener not registered"); return new Promise((resolve) => { listener!(message, { id: "peach-extension" } as chrome.runtime.MessageSender, resolve); }); } it("rejects PAIRING_COMPLETED for an unverified, unpaired publicKeyHex", async () => { const fakeKey = "deadbeef".repeat(8); const r = await dispatch({ type: "PAIRING_COMPLETED", payload: { deviceName: "Attacker", publicKeyHex: fakeKey }, }); expect(r.ok).toBe(false); expect(r.error).toBe("pairing_not_verified"); // Did not write to paired devices. const paired = (storage.peach_paired_devices as unknown[] | undefined) ?? []; expect(paired.length).toBe(0); }); it("accepts PAIRING_COMPLETED for a freshly verified handshake", async () => { const messages = await import("./messages"); const goodKey = "abcdef01".repeat(8); messages.markPairingHandshakeVerified(goodKey); const r = await dispatch({ type: "PAIRING_COMPLETED", payload: { deviceName: "RealDevice", publicKeyHex: goodKey }, }); expect(r.ok).toBe(true); const paired = (storage.peach_paired_devices as Array> | undefined) ?? []; expect(paired.some(d => d.x25519PublicKey === goodKey)).toBe(true); }); it("verified marker is single-use — second PAIRING_COMPLETED after the first device is removed is rejected", async () => { const messages = await import("./messages"); const goodKey = "12345678".repeat(8); messages.markPairingHandshakeVerified(goodKey); const first = await dispatch({ type: "PAIRING_COMPLETED", payload: { deviceName: "Dev", publicKeyHex: goodKey }, }); expect(first.ok).toBe(true); // Wipe paired state so alreadyPaired is false. delete storage.peach_paired_devices; const second = await dispatch({ type: "PAIRING_COMPLETED", payload: { deviceName: "Dev", publicKeyHex: goodKey }, }); expect(second.ok).toBe(false); expect(second.error).toBe("pairing_not_verified"); }); it("missing publicKeyHex is rejected", async () => { const r = await dispatch({ type: "PAIRING_COMPLETED", payload: { deviceName: "X" }, }); expect(r.ok).toBe(false); expect(r.error).toBe("publickeyhex_required"); }); });