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
↓ peach-browser-0dcc53fd.tar.gzatlas at this save
files changed6 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 −0manifest.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": {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",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",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");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);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.