8989d7885b21

fix: PCH-01-013 enforce 16/32 fingerprint length before payload comparison in reconstructAndVerify — empty/short declared fingerprint no longer passes trivially (AUD-004 parity) (v3.8.1)

time
2026-06-10 19:54 UTC · 4 months ago
machine
koh2
agent
claude-opus-4-8
session
pch-01 fix integration
parent
3d3cfda6a120
2 files src/popup/codex/codex.adversarial.test.ts+22 −0src/popup/codex/restore.ts+12 −0
src/popup/codex/codex.adversarial.test.ts+22 −0
@@ -449,6 +449,28 @@449449     expect(reconstructed.entryCount).toBe(payload.entryCount);450450   });451451 452+  // PCH-01-013: the declared fingerprint length is attacker-controlled. An453+  // empty fingerprint must not pass via slice(0,0) === "".454+  it("reconstruct_rejects_empty_fingerprint", async () => {455+    const base = await makePayload(2000);456+    const payload = { ...base, vaultFingerprint: "" };457+    const envs = await buildEnvelopesForPayload(payload);458+    const buf = new ChunkBuffer();459+    for (const e of envs) await buf.add(e);460+    await expect(reconstructAndVerify(buf)).rejects.toThrow(CodexError);461+  });462+463+  it("reconstruct_accepts_current_fingerprint_length", async () => {464+    const base = await makePayload(2500);465+    const fp32 = (await sha256Hex(new TextEncoder().encode(base.vaultBlob))).slice(0, 32).toUpperCase();466+    const payload = { ...base, vaultFingerprint: fp32 };467+    const envs = await buildEnvelopesForPayload(payload);468+    const buf = new ChunkBuffer();469+    for (const e of envs) await buf.add(e);470+    const reconstructed = await reconstructAndVerify(buf);471+    expect(reconstructed.vaultFingerprint).toBe(fp32);472+  });473+452474   it("missing chunks but enough parity still reconstructs", async () => {453475     const payload = await makePayload(5000);454476     const envs = await buildEnvelopesForPayload(payload);

view file

src/popup/codex/restore.ts+12 −0
@@ -333,6 +333,18 @@333333   // fingerprint; current Codexes carry 32 chars (128 bits). Slice by the334334   // stored length so both verify without an envelope-version bump.335335   const fingerprintLen = payload.vaultFingerprint.length;336+  // AUD-004 / PCH-01-013: the declared fingerprint length is attacker-337+  // controlled. Without this gate, an empty declared fingerprint passes338+  // trivially (slice(0,0) === "") and a 1-char one passes with p=1/16. Accept339+  // only the two documented widths: 16 (legacy 64-bit) and 32 (current 128-bit).340+  // Mirrors verifyChunkIntegrity in helpers.ts.341+  if (fingerprintLen !== 16 && fingerprintLen !== 32) {342+    throw new CodexError(343+      "RECONSTRUCTION_FINGERPRINT_MISMATCH",344+      "Reconstructed vault fingerprint has an invalid length. " +345+        "Try scanning the Codex again from the beginning.",346+    );347+  }336348   const fingerprint = (await sha256Hex(new TextEncoder().encode(payload.vaultBlob)))337349     .slice(0, fingerprintLen)338350     .toUpperCase();

view file