8dda840a1a99
AUD-006: reject sync timestamps beyond 5-minute clock skew budget
- time
- 2026-05-16 13:45 UTC · 5 months ago
- machine
- koh2
- parent
- 70c8750fd4f7
↓ peach-browser-8dda840a.tar.gzatlas at this save
files changed2 files
src/background/sync.adversarial.test.ts+53 −14src/background/vault.ts+13 −0src/background/sync.adversarial.test.ts+53 −14
@@ -188,13 +188,56 @@188188 });189189 }190190 191- it("far-future timestamp accepted (no upper bound on remote ts)", async () => {192- const vault = await setupBlobs();193- const applied = await vault.applyIncomingVault(194- makeIncomingBlob(),195- Date.now() * 100196- );197- expect(applied).toBe(true);191+ // AUD-006: applyIncomingVault now bounds the remote timestamp by a192+ // 5-minute clock-skew budget so a paired peer with a bad/malicious clock193+ // cannot pin last-write-wins permanently with Number.MAX_SAFE_INTEGER.194+ describe("AUD-006: timestamp skew budget", () => {195+ it("rejects remote timestamp beyond 5-minute skew budget", async () => {196+ const vault = await setupBlobs();197+ const farFuture = Date.now() + 6 * 60 * 1_000;198+ const applied = await vault.applyIncomingVault(199+ makeIncomingBlob(),200+ farFuture201+ );202+ expect(applied).toBe(false);203+ });204+205+ it("rejects Number.MAX_SAFE_INTEGER timestamp", async () => {206+ const vault = await setupBlobs();207+ const applied = await vault.applyIncomingVault(208+ makeIncomingBlob(),209+ Number.MAX_SAFE_INTEGER210+ );211+ expect(applied).toBe(false);212+ });213+214+ it("rejects Date.now() * 100 (previously accepted)", async () => {215+ const vault = await setupBlobs();216+ const applied = await vault.applyIncomingVault(217+ makeIncomingBlob(),218+ Date.now() * 100219+ );220+ expect(applied).toBe(false);221+ });222+223+ it("accepts remote timestamp within 5-minute skew budget", async () => {224+ const vault = await setupBlobs();225+ const slightlyFuture = Date.now() + 4 * 60 * 1_000;226+ const applied = await vault.applyIncomingVault(227+ makeIncomingBlob(),228+ slightlyFuture229+ );230+ expect(applied).toBe(true);231+ });232+233+ it("accepts legitimately newer remote timestamp", async () => {234+ const vault = await setupBlobs();235+ const applied = await vault.applyIncomingVault(236+ makeIncomingBlob(),237+ 1_700_000_000_000 + 10_000238+ );239+ expect(applied).toBe(true);240+ });198241 });199242 });200243 @@ -482,13 +525,9 @@482525 // ── Newer-timestamp sweep ────────────────────────────────────────────────483526 484527 describe("timestamp boundary sweep", () => {485- for (const baseTs of [486- 0,487- 1,488- 1_000,489- 1_700_000_000_000,490- Number.MAX_SAFE_INTEGER - 10,491- ]) {528+ // AUD-006: MAX_SAFE_INTEGER is now beyond the 5-min skew budget, so529+ // "newer is accepted" only holds for timestamps not far in the future.530+ for (const baseTs of [0, 1, 1_000, 1_700_000_000_000]) {492531 it(`base ts ${baseTs}: applies when remote is strictly newer`, async () => {493532 const vault = await import("./vault");494533 await vault.createVault("test-pass-12345");src/background/vault.ts+13 −0
@@ -2281,6 +2281,19 @@22812281 }22822282 }22832283 2284+ // AUD-006: reject implausibly future timestamps. A paired peer with a bad2285+ // clock — or a malicious peer — can send Number.MAX_SAFE_INTEGER and pin2286+ // last-write-wins so no local change ever wins. 5 minutes covers legitimate2287+ // clock drift; anything further is implausible.2288+ const SYNC_CLOCK_SKEW_BUDGET_MS = 5 * 60 * 1_000;2289+ if (remoteTimestamp > Date.now() + SYNC_CLOCK_SKEW_BUDGET_MS) {2290+ console.warn(2291+ '[Peach] sync rejected: remote timestamp too far in future',2292+ { remoteTimestamp, now: Date.now(), delta: remoteTimestamp - Date.now() },2293+ );2294+ return false;2295+ }2296+22842297 if (remoteTimestamp <= localTimestamp) return false;22852298 22862299 // Parse + shape-validate before any storage mutation. A malformed blob