0dcc53fd9e7f

record which peer timestamp each bound covers, and clamp revocation timestamps for display (v4.1.5)

time
2026-07-26 08:32 UTC · 3 months ago
machine
koh2
agent
claude-opus-5
session
sync campaign remaining items
parent
1ef99bf947b0
6 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/paired-devices.test.ts+9 −0src/background/paired-devices.ts+7 −1src/shared/sync-limits.ts+24 −0
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "4.1.4",4+  "version": "4.1.5",55   "description": "Zero-knowledge password manager",66   "browser_specific_settings": {77     "gecko": {

view file

manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "4.1.4",4+  "version": "4.1.5",55   "description": "Zero-knowledge password manager",66   "action": {77     "default_popup": "src/popup/index.html",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "4.1.4",3+  "version": "4.1.5",44   "description": "Zero-knowledge password manager",55   "license": "GPL-3.0-only",66   "type": "module",

view file

src/background/paired-devices.test.ts+9 −0
@@ -225,6 +225,15 @@225225       expect(records[0]).toMatchObject({ revokedAt: 1, revokedByDeviceId: "x" });226226     });227227 228+    it("HOLE 2: a far-future revokedAt is clamped rather than stored verbatim", async () => {229+      const farFuture = Date.now() + 10 * 365 * 24 * 60 * 60 * 1000;230+      await mergeDeviceRevocations([231+        { deviceId: "a", longTermPublicKeyHex: "a".repeat(64), revokedAt: farFuture, revokedByDeviceId: "peer" },232+      ]);233+      const [record] = await listDeviceRevocations();234+      expect(record.revokedAt).toBeLessThanOrEqual(Date.now() + 5 * 60 * 1000);235+    });236+228237     it("clearDeviceRevocation lifts the block so a direct re-pair can readmit", async () => {229238       await upsertPairedDevice(SAMPLE);230239       await revokeDeviceById(SAMPLE.deviceId, "this-device");

view file

src/background/paired-devices.ts+7 −1
@@ -22,6 +22,7 @@2222 2323 import type { DeviceRevocation, PairedDevice } from "../shared/sync-types";2424 import { constantTimeEqualHex } from "../shared/constant-time";25+import { clampSyncTimestamp } from "../shared/sync-limits";2526 2627 const REGISTRY_KEY = "peach_paired_devices_v3";2728 const REVOCATIONS_KEY = "peach_device_revocations_v1";@@ -180,7 +181,12 @@180181 ): Promise<string[]> {181182   const existing = await listDeviceRevocations();182183   const known = new Set(existing.map((r) => r.deviceId));183-  const fresh = incoming.filter((r) => !known.has(r.deviceId));184+  const fresh = incoming185+    .filter((r) => !known.has(r.deviceId))186+    // Clamped for display sanity only — a year-9999 revokedAt would render as187+    // nonsense. The protection against ordering games is that revocation is188+    // terminal, not this clamp (HOLE 2 audit in sync-limits.ts).189+    .map((r) => ({ ...r, revokedAt: clampSyncTimestamp(r.revokedAt) }));184190   if (fresh.length === 0) return [];185191   await storeRevocations([...existing, ...fresh].slice(-MAX_REVOCATIONS));186192   return fresh.map((r) => r.deviceId);

view file

src/shared/sync-limits.ts+24 −0
@@ -25,6 +25,30 @@2525 }2626 2727 /**28+ * HOLE 2 coverage audit — every peer-supplied timestamp in the sync protocol,29+ * and what stops it influencing ordering. Re-check this list when adding a30+ * timestamp to any wire type.31+ *32+ *   entry.modifiedAt      clamped (vault.ts applyMergedRemoteEntries)33+ *   entry.deletedAt       clamped (vault.ts applyMergedRemoteEntries)34+ *   tombstone ts          clamped twice — sync-vault-adapter.ts boundary and35+ *                         again in vault.ts, deliberately36+ *   manifest.issuedAt     NOT clamped, and must not be: it is checked as a37+ *                         two-sided freshness window (|now - issuedAt| >38+ *                         DEVICE_MANIFEST_FRESHNESS_MS rejects the manifest39+ *                         outright). Rejecting is strictly stronger than40+ *                         clamping, and it never participates in ordering.41+ *   entry.pairedAt        never compared — stored and displayed only. A peer42+ *                         cannot reorder anything with it, and the manifest43+ *                         upsert keeps `existing.pairedAt` when we already44+ *                         know the device.45+ *   revocation.revokedAt  clamped on merge for display sanity. Revocation is46+ *                         terminal, so it is not an ordering input by47+ *                         construction — that is the actual protection.48+ *   SYNC_HELLO.deviceTime warning only; never merged or compared for ordering.49+ */50+51+/**2852  * MV-3: hard resource bounds on the sync receive path. A paired/vouched2953  * peer (or anything that passes the Noise KK mutual static-key pin) must not be3054  * able to OOM the service worker with one frame or one giant manifest.

view file