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    "ADD_ENTRY",53    "UPDATE_ENTRY",54    "DELETE_ENTRY",55    "CLEAR_VAULT_ENTRIES",56    "RESTORE_FROM_CODEX",57    "CHANGE_PASSWORD",58    "RECOVER_WITH_WORDS",59    "SETUP_DURESS",60    "PAIRING_COMPLETED",61    "ACTIVATE_LICENSE",62    "TRIGGER_SYNC",63    "GET_IDENTITIES",64    "SEARCH_VAULT",65    "GET_ENTRY_FOR_FILL",66    "LOCAL_WEBSOCKET_PROBE",67    "SUBNET_DETECT_PROBE",68    "",69    "TOTALLY_MADE_UP",70  ];7172  for (const type of allowed) {73    it(`forwards allowed message: ${type}`, async () => {74      await fire({ type });75      expect(mockSendMessage).toHaveBeenCalledWith({ type });76    });77  }7879  for (const type of blocked) {80    it(`blocks message: ${type || "(empty)"}`, async () => {81      await fire({ type });82      expect(mockSendMessage).not.toHaveBeenCalled();83    });84  }8586  it("blocks null payload", async () => {87    await fire(null);88    expect(mockSendMessage).not.toHaveBeenCalled();89  });9091  it("blocks empty payload object", async () => {92    await fire({});93    expect(mockSendMessage).not.toHaveBeenCalled();94  });9596  it("blocks cross-frame messages", async () => {97    await fire({ type: "PASSKEY_GET" }, null);98    expect(mockSendMessage).not.toHaveBeenCalled();99  });100101  it("does not post a response for blocked messages", async () => {102    const spy = vi.spyOn(window, "postMessage");103    await fire({ type: "GET_ENTRIES" });104    expect(spy).not.toHaveBeenCalled();105    spy.mockRestore();106  });107});