eeb12f15f2c4
add protocolVersion to SYNC_HELLO, reject stale peers with update-required error
- time
- 2026-09-08 22:08 UTC · 1 month ago
- machine
- koh2
- agent
- hf:zai-org/GLM-5.2
- session
- sync protocol version
- parent
- 2c0d1e890d44
↓ peach-browser-eeb12f15.tar.gzatlas at this save
files changed11 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/sync-coordinator.test.ts+39 −10src/background/sync-coordinator.ts+11 −0src/background/sync-runner.test.ts+2 −0src/background/sync-trigger.test.ts+1 −0src/background/sync-trigger.ts+1 −0src/shared/sync-outcome.test.ts+1 −0src/shared/sync-outcome.ts+3 −0src/shared/sync-types.ts+9 −0manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "5.2.21",4+ "version": "5.2.23",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -2,7 +2,7 @@22 "manifest_version": 3,33 "minimum_chrome_version": "116",44 "name": "Peach Password Manager",5- "version": "5.2.21",5+ "version": "5.2.23",66 "description": "Zero-knowledge password manager",77 "action": {88 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "5.2.22",3+ "version": "5.2.23",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",src/background/sync-coordinator.test.ts+39 −10
@@ -149,6 +149,7 @@149149 deviceId: "p",150150 version: 0,151151 deviceTime: Date.now(),152+ protocolVersion: 2,152153 });153154 await peer.receive(); // our manifest154155 await peer.send({ type: "SYNC_MANIFEST", entries: {}, tombstones: {} });@@ -188,6 +189,7 @@188189 deviceId: "p",189190 version: 0,190191 deviceTime: Date.now(),192+ protocolVersion: 2,191193 });192194 await peer.receive(); // our manifest193195 await peer.send({ type: "SYNC_MANIFEST", entries: {}, tombstones: {} });@@ -219,6 +221,7 @@219221 deviceId: "p",220222 version: 0,221223 deviceTime: Date.now(),224+ protocolVersion: 2,222225 });223226 await peer.receive();224227 const entries = Object.fromEntries(@@ -241,6 +244,7 @@241244 deviceId: "p",242245 version: 0,243246 deviceTime: Date.now(),247+ protocolVersion: 2,244248 });245249 await tombstonePeer.receive();246250 const tombstones = Object.fromEntries(@@ -270,7 +274,7 @@270274 expect(sentHello.deviceTime).toBeGreaterThan(0);271275 }272276 // Send a mismatched vaultId to abort early so the test doesn't hang.273- await peer.send({ type: "SYNC_HELLO", vaultId: "different-vault", deviceId: "p", version: 7, deviceTime: Date.now() });277+ await peer.send({ type: "SYNC_HELLO", vaultId: "different-vault", deviceId: "p", version: 7, deviceTime: Date.now(), protocolVersion: 2 });274278 await peer.receive(); // SYNC_ERROR275279 await initPromise;276280 });@@ -282,7 +286,7 @@282286 const initPromise = runSyncInitiator(session, vault);283287 284288 await peer.receive(); // SYNC_HELLO285- await peer.send({ type: "SYNC_HELLO", vaultId: "vault-abc", deviceId: "p", version: 10, deviceTime: Date.now() });289+ await peer.send({ type: "SYNC_HELLO", vaultId: "vault-abc", deviceId: "p", version: 10, deviceTime: Date.now(), protocolVersion: 2 });286290 287291 // Coordinator should send SYNC_DONE without any manifest exchange.288292 const done = await peer.receive();@@ -357,8 +361,9 @@357361 vaultId: "vault-abc",358362 deviceId: "p",359363 version: 3, // different version so exchange proceeds360- deviceTime: Date.now() - 200_000,361- });364+ deviceTime: Date.now() - 200_000,365+ protocolVersion: 2,366+ });362367 // Yield so the coordinator's async continuation can run the clock-skew check.363368 await new Promise((r) => setTimeout(r, 0));364369 expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("clock skew"));@@ -440,7 +445,7 @@440445 const peerProtocol = (async () => {441446 const hello = await peer.receive();442447 expect(hello.type).toBe("SYNC_HELLO");443- await peer.send({ type: "SYNC_HELLO", vaultId: "vault-abc", deviceId: "device-peer", version: 3, deviceTime: Date.now() });448+ await peer.send({ type: "SYNC_HELLO", vaultId: "vault-abc", deviceId: "device-peer", version: 3, deviceTime: Date.now(), protocolVersion: 2 });444449 445450 const ourManifest = await peer.receive();446451 expect(ourManifest.type).toBe("SYNC_MANIFEST");@@ -499,7 +504,7 @@499504 const peerProtocol = (async () => {500505 const hello = await peer.receive();501506 expect(hello.type).toBe("SYNC_HELLO");502- await peer.send({ type: "SYNC_HELLO", vaultId: "vault-B", deviceId: "evil", version: 3, deviceTime: Date.now() });507+ await peer.send({ type: "SYNC_HELLO", vaultId: "vault-B", deviceId: "evil", version: 3, deviceTime: Date.now(), protocolVersion: 2 });503508 // Peer should receive a SYNC_ERROR and we should stop.504509 const err = await peer.receive();505510 expect(err.type).toBe("SYNC_ERROR");@@ -510,6 +515,30 @@510515 expect(outcome.status).toBe("vault_mismatch");511516 });512517 518+ it("rejects a stale peer with PEER_PROTOCOL_OUTDATED", async () => {519+ const [session, peer] = inMemoryPair();520+ const vault = makeVaultAdapter({});521+522+ const initPromise = runSyncInitiator(session, vault);523+ const peerProtocol = (async () => {524+ await peer.receive(); // hello525+ await peer.send({526+ type: "SYNC_HELLO",527+ vaultId: "vault-abc",528+ deviceId: "p",529+ version: 3,530+ deviceTime: Date.now(),531+ });532+ const err = await peer.receive();533+ expect(err.type).toBe("SYNC_ERROR");534+ })();535+536+ const outcome = await initPromise;537+ await peerProtocol;538+ expect(outcome.status).toBe("errored");539+ expect(outcome.error).toBe("PEER_PROTOCOL_OUTDATED");540+ });541+513542 it("exchanges device manifests when both adapters opt in", async () => {514543 const [session, peer] = inMemoryPair();515544 const ourKp = await deriveSyncSigningKeypair(new Uint8Array(32).fill(0x11), "us");@@ -532,7 +561,7 @@532561 const initPromise = runSyncInitiator(session, vault);533562 const peerProtocol = (async () => {534563 await peer.receive(); // hello535- await peer.send({ type: "SYNC_HELLO", vaultId: "vault-abc", deviceId: "p", version: 3, deviceTime: Date.now() });564+ await peer.send({ type: "SYNC_HELLO", vaultId: "vault-abc", deviceId: "p", version: 3, deviceTime: Date.now(), protocolVersion: 2 });536565 await peer.receive(); // manifest537566 await peer.send({ type: "SYNC_MANIFEST", entries: {}, tombstones: {} });538567 await peer.receive(); // request@@ -569,7 +598,7 @@569598 const initPromise = runSyncInitiator(session, vault);570599 const peerProtocol = (async () => {571600 await peer.receive();572- await peer.send({ type: "SYNC_HELLO", vaultId: "vault-abc", deviceId: "p", version: 3, deviceTime: Date.now() });601+ await peer.send({ type: "SYNC_HELLO", vaultId: "vault-abc", deviceId: "p", version: 3, deviceTime: Date.now(), protocolVersion: 2 });573602 await peer.receive();574603 await peer.send({ type: "SYNC_MANIFEST", entries: {}, tombstones: {} });575604 await peer.receive();@@ -597,7 +626,7 @@597626 const initPromise = runSyncInitiator(session, vault);598627 void (async () => {599628 await peer.receive(); // hello600- await peer.send({ type: "SYNC_HELLO", vaultId: "v", deviceId: "p", version: 3, deviceTime: Date.now() });629+ await peer.send({ type: "SYNC_HELLO", vaultId: "v", deviceId: "p", version: 3, deviceTime: Date.now(), protocolVersion: 2 });601630 await peer.receive(); // our manifest602631 await peer.send({ type: "SYNC_DONE" }); // wrong frame603632 })();@@ -620,7 +649,7 @@620649 const initPromise = runSyncInitiator(session, vault);621650 void (async () => {622651 await peer.receive(); // hello623- await peer.send({ type: "SYNC_HELLO", vaultId: "vault-abc", deviceId: "p", version: 5, deviceTime: Date.now() });652+ await peer.send({ type: "SYNC_HELLO", vaultId: "vault-abc", deviceId: "p", version: 5, deviceTime: Date.now(), protocolVersion: 2 });624653 await peer.receive(); // our done625654 // No SYNC_DONE echo — Android just hangs up.626655 await peer.closeInitiatorReceive();src/background/sync-coordinator.ts+11 −0
@@ -21,6 +21,7 @@2121 2222 import {2323 DEVICE_MANIFEST_VERSION,24+ SYNC_PROTOCOL_VERSION,2425 type SyncMessage,2526 type SignedDeviceManifest,2627 } from "../shared/sync-types";@@ -135,6 +136,7 @@135136 deviceId: ourDeviceId,136137 version: localVersion,137138 deviceTime: localTime,139+ protocolVersion: SYNC_PROTOCOL_VERSION,138140 deviceManifestVersion: supportsDeviceManifest139141 ? DEVICE_MANIFEST_VERSION140142 : undefined,@@ -144,6 +146,10 @@144146 if (helloIn.type !== "SYNC_HELLO") {145147 return errorOutcome(result, `expected SYNC_HELLO, got ${helloIn.type}`);146148 }149+ if ((helloIn.protocolVersion ?? 1) < SYNC_PROTOCOL_VERSION) {150+ await sendMessage(session, { type: "SYNC_ERROR", error: "PEER_PROTOCOL_OUTDATED" });151+ return errorOutcome(result, "PEER_PROTOCOL_OUTDATED");152+ }147153 if (helloIn.vaultId !== ourVaultId) {148154 await sendMessage(session, { type: "SYNC_ERROR", error: "vault_id_mismatch" });149155 result.status = "vault_mismatch";@@ -333,12 +339,17 @@333339 if (helloIn.type !== "SYNC_HELLO") {334340 return errorOutcome(result, `expected SYNC_HELLO, got ${helloIn.type}`);335341 }342+ if ((helloIn.protocolVersion ?? 1) < SYNC_PROTOCOL_VERSION) {343+ await sendMessage(session, { type: "SYNC_ERROR", error: "PEER_PROTOCOL_OUTDATED" });344+ return errorOutcome(result, "PEER_PROTOCOL_OUTDATED");345+ }336346 await sendMessage(session, {337347 type: "SYNC_HELLO",338348 vaultId: ourVaultId,339349 deviceId: ourDeviceId,340350 version: localVersion,341351 deviceTime: localTime,352+ protocolVersion: SYNC_PROTOCOL_VERSION,342353 deviceManifestVersion: supportsDeviceManifest343354 ? DEVICE_MANIFEST_VERSION344355 : undefined,src/background/sync-runner.test.ts+2 −0
@@ -121,6 +121,7 @@121121 deviceId: "peer-device",122122 version: 0,123123 deviceTime: Date.now(),124+ protocolVersion: 2,124125 });125126 const manifest = await peer.receive();126127 if (manifest.type !== "SYNC_MANIFEST") throw new Error("expected MANIFEST");@@ -354,6 +355,7 @@354355 deviceId: "peer-device",355356 version: 0,356357 deviceTime: Date.now(),358+ protocolVersion: 2,357359 });358360 }359361 })().catch(() => undefined);src/background/sync-trigger.test.ts+1 −0
@@ -315,6 +315,7 @@315315 ["sync_disabled", "sync_disabled"],316316 ["private_relay_unset", "private_relay_unset"],317317 ["vault_id_mismatch", "vault_id_mismatch"],318+ ["PEER_PROTOCOL_OUTDATED", "peer_protocol_outdated"],318319 ["sync_timeout", "sync_timeout"],319320 ["VAULT_LOCKED", "vault_locked"],320321 ["relay_sync_failed:sync_handshake_timeout", "handshake_failed"],src/background/sync-trigger.ts+1 −0
@@ -272,6 +272,7 @@272272 if (error.includes("sync_disabled")) return "sync_disabled";273273 if (error.includes("private_relay_unset")) return "private_relay_unset";274274 if (error.includes("vault_id_mismatch")) return "vault_id_mismatch";275+ if (error.includes("PEER_PROTOCOL_OUTDATED")) return "peer_protocol_outdated";275276 if (error.includes("sync_timeout")) return "sync_timeout";276277 // A transport that hung up mid-protocol. The alarm retries, and the peer may277278 // well have applied what we already sent, so this is network-class (yellow)src/shared/sync-outcome.test.ts+1 −0
@@ -93,6 +93,7 @@9393 relay_unreachable: "warn",9494 handshake_failed: "warn",9595 vault_id_mismatch: "error",96+ peer_protocol_outdated: "error",9697 sync_timeout: "warn",9798 connection_lost: "warn",9899 run_failed: "error",src/shared/sync-outcome.ts+3 −0
@@ -17,6 +17,7 @@1717 | "relay_unreachable"1818 | "handshake_failed"1919 | "vault_id_mismatch"20+ | "peer_protocol_outdated"2021 | "sync_timeout"2122 | "connection_lost"2223 | "run_failed";@@ -35,6 +36,7 @@3536 "runner_unavailable",3637 "private_relay_unset",3738 "vault_id_mismatch",39+ "peer_protocol_outdated",3840 "run_failed",3941 ]);4042 @@ -141,6 +143,7 @@141143 relay_unreachable: "Couldn't reach the relay",142144 handshake_failed: "Reached the relay, but the device didn't answer",143145 vault_id_mismatch: "That device holds a different vault",146+ peer_protocol_outdated: "Your other device needs to update before it can sync",144147 sync_timeout: "The device stopped responding mid-sync",145148 connection_lost: "The connection dropped mid-sync — will retry",146149 run_failed: "Sync failed",src/shared/sync-types.ts+9 −0
@@ -89,6 +89,13 @@8989 version: number;9090 deviceTime: number;9191 /**92+ * Wire protocol version, incremented when the sync sequence itself93+ * changes. Distinct from `version` (vault data version) and94+ * `deviceManifestVersion`. Old clients omit this field; treat missing95+ * as 1.96+ */97+ protocolVersion?: number;98+ /**9299 * Advertises equal-version device-graph exchange support. Older peers93100 * omit this field and retain the legacy SYNC_DONE fast path.94101 */@@ -109,6 +116,8 @@109116 /** 0x03 adds each peer edge's relay URL so a newly introduced pair can agree110117 * on a route without depending on the introducing device. Coordinated wire111118 * change with peach-android. */119+export const SYNC_PROTOCOL_VERSION = 2 as const;120+112121 export const DEVICE_MANIFEST_VERSION = 0x04 as const;113122 export const MAX_VOUCH_DEPTH = 3;114123 /** Manifests older than this are silently rejected (v2 brief §4.3). */