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
2 files src/background/sync.adversarial.test.ts+53 −14src/background/vault.ts+13 −0
src/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");

view file

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

view file