passkeys-firefox-relay.test.ts

2.7 KB · raw

1// @vitest-environment jsdom23import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";45const mockSendMessage = vi.fn().mockResolvedValue({ ok: true });67vi.stubGlobal("chrome", {8  runtime: {9    id: "ext-id",10    sendMessage: mockSendMessage,11  },12});1314beforeAll(async () => {15  await import("./passkeys-firefox-relay");16});1718async function fire(payload: unknown, source: Window | null = window) {19  window.dispatchEvent(20    new MessageEvent("message", {21      data: { type: "PEACH_PASSKEY_REQUEST", id: "x", payload },22      source,23    })24  );25  await new Promise((resolve) => setTimeout(resolve, 10));26}2728describe("Firefox passkey relay allowlist", () => {29  beforeEach(() => {30    mockSendMessage.mockClear();31  });3233  const allowed = [34    "PASSKEY_CREATE",35    "PASSKEY_GET",36    "PASSKEY_HAS_CREDENTIALS",37    "INITIATE_FIREFOX_PASSKEY_CREATE",38    "PASSKEY_OFFER_DECLINE",39    "GET_PASSKEY_OFFER_DECLINED",40    "GET_PASSKEY_FOR_DOMAIN",41  ];4243  const blocked = [44    // Popup-only: never page-relayable (Codex Security 2026-08-04).45    "COMPLETE_FIREFOX_PASSKEY_CREATE",46    // Approval originates from the extension toast/popup, never the page.47    // Relayable requestId brute-force → consent bypass (Audit 2026-08-20 #1).48    "PASSKEY_AUTH_RESPONSE",49    "GET_ENTRIES",50    "GET_RECOVERY_WORDS",51    "DESTROY_VAULT",52    "GET_DERIVED_KEY",53    "ADD_ENTRY",54    "UPDATE_ENTRY",55    "DELETE_ENTRY",56    "CLEAR_VAULT_ENTRIES",57    "RESTORE_FROM_CODEX",58    "CHANGE_PASSWORD",59    "RECOVER_WITH_WORDS",60    "SETUP_DURESS",61    "PAIRING_COMPLETED",62    "ACTIVATE_LICENSE",63    "TRIGGER_SYNC",64    "GET_IDENTITIES",65    "SEARCH_VAULT",66    "GET_ENTRY_FOR_FILL",67    "LOCAL_WEBSOCKET_PROBE",68    "SUBNET_DETECT_PROBE",69    "",70    "TOTALLY_MADE_UP",71  ];7273  for (const type of allowed) {74    it(`forwards allowed message: ${type}`, async () => {75      await fire({ type });76      expect(mockSendMessage).toHaveBeenCalledWith({ type });77    });78  }7980  for (const type of blocked) {81    it(`blocks message: ${type || "(empty)"}`, async () => {82      await fire({ type });83      expect(mockSendMessage).not.toHaveBeenCalled();84    });85  }8687  it("blocks null payload", async () => {88    await fire(null);89    expect(mockSendMessage).not.toHaveBeenCalled();90  });9192  it("blocks empty payload object", async () => {93    await fire({});94    expect(mockSendMessage).not.toHaveBeenCalled();95  });9697  it("blocks cross-frame messages", async () => {98    await fire({ type: "PASSKEY_GET" }, null);99    expect(mockSendMessage).not.toHaveBeenCalled();100  });101102  it("does not post a response for blocked messages", async () => {103    const spy = vi.spyOn(window, "postMessage");104    await fire({ type: "GET_ENTRIES" });105    expect(spy).not.toHaveBeenCalled();106    spy.mockRestore();107  });108});