6d494603f74d

fix: PCH-02-007 add Add-Padding header to HIBP k-anonymity range check (v4.0.0)

time
2026-06-12 20:34 UTC · 4 months ago
machine
koh2
agent
gpt-5
session
pch-02 integration
parent
16d51870a784
2 files src/background/vault.hibp-padding.test.ts+80 −0src/background/vault.ts+5 −1
src/background/vault.hibp-padding.test.ts+80 −0
@@ -0,0 +1,80 @@1+// PCH-02-007 regression tests — the HIBP k-anonymity range request must ask2+// for response padding, and the parser must stay correct on padded responses.3+4+import { beforeEach, describe, expect, it, vi } from "vitest";5+6+// SHA-1("password") = 5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD87+const PREFIX = "5BAA6";8+const SUFFIX = "1E4C9B93F3F0682250B6CF8331B7EE68FD8";9+10+describe("PCH-02-007 — HIBP Add-Padding", () => {11+  let fetchMock: ReturnType<typeof vi.fn>;12+13+  beforeEach(() => {14+    vi.resetModules();15+16+    const chromeMock = {17+      storage: {18+        local: {19+          get: vi.fn(async () => ({})),20+          set: vi.fn(async () => undefined),21+          remove: vi.fn(async () => undefined),22+        },23+      },24+      alarms: {25+        clear: vi.fn(),26+        create: vi.fn(),27+        onAlarm: { addListener: vi.fn() },28+      },29+      idle: {30+        setDetectionInterval: vi.fn(),31+        onStateChanged: { addListener: vi.fn() },32+      },33+    } as unknown as typeof chrome;34+    (globalThis as { chrome?: typeof chrome }).chrome = chromeMock;35+36+    fetchMock = vi.fn(async () => ({37+      ok: true,38+      text: async () =>39+        [40+          "0018A45C4D1DEF81644B54AB7F969B88D65:1",41+          `${SUFFIX}:3303003`,42+          "FFFFFFAAAAAABBBBBBCCCCCCDDDDDDEEEEE:0",43+        ].join("\r\n"),44+    }));45+    vi.stubGlobal("fetch", fetchMock);46+  });47+48+  it("sends Add-Padding: true on the range request", async () => {49+    const vault = await import("./vault");50+    await vault.checkBreach("password");51+52+    expect(fetchMock).toHaveBeenCalledWith(53+      `https://api.pwnedpasswords.com/range/${PREFIX}`,54+      expect.objectContaining({55+        headers: expect.objectContaining({ "Add-Padding": "true" }),56+      }),57+    );58+  });59+60+  it("still parses the real breach count out of a padded response", async () => {61+    const vault = await import("./vault");62+    const count = await vault.checkBreach("password");63+    expect(count).toBe(3303003);64+  });65+66+  it("reports 0 when the response holds only padding lines", async () => {67+    fetchMock.mockImplementationOnce(async () => ({68+      ok: true,69+      text: async () =>70+        [71+          "FFFFFFAAAAAABBBBBBCCCCCCDDDDDDEEEEE:0",72+          "EEEEEEAAAAAABBBBBBCCCCCCDDDDDDFFFFF:0",73+        ].join("\r\n"),74+    }));75+76+    const vault = await import("./vault");77+    const count = await vault.checkBreach("password");78+    expect(count).toBe(0);79+  });80+});

view file

src/background/vault.ts+5 −1
@@ -227,7 +227,11 @@227227   const suffix = hashHex.slice(5);228228 229229   try {230-    const res = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`);230+    // PCH-02-007: request HIBP response padding so response length does not231+    // reveal how many suffixes a queried prefix returned.232+    const res = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`, {233+      headers: { "Add-Padding": "true" },234+    });231235     if (!res.ok) return 0;232236     const text = await res.text();233237     const match = text.split("\r\n").find((l) => l.startsWith(suffix));

view file