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
↓ peach-browser-8989d788.tar.gzatlas at this save
files changed2 files
src/popup/codex/codex.adversarial.test.ts+22 −0src/popup/codex/restore.ts+12 −0src/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);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();