9aec7adfb69c

fix peer revoke and reset identity migration failures (v5.2.47)

time
2026-09-11 17:01 UTC · 1 month ago
machine
koh2
agent
camel-stream/auto
session
fix peer revoke reset
parent
b8afc9544b71
7 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/device-identity.ts+6 −8src/background/messages.test.ts+27 −2src/background/messages.ts+12 −4src/shared/release-notes.ts+8 −0
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "5.2.46",4+  "version": "5.2.47",55   "description": "Zero-knowledge password manager",66   "browser_specific_settings": {77     "gecko": {

view file

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.46",5+  "version": "5.2.47",66   "description": "Zero-knowledge password manager",77   "action": {88     "default_popup": "src/popup/index.html",

view file

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

view file

src/background/device-identity.ts+6 −8
@@ -229,17 +229,15 @@229229  */230230 setGenerationIdentity({231231   async ensureIdentity() {232+    if (await hasLegacyTrustState(browserGenerationIo)) return false;232233     try {233-      await buildLocalIdentity();234+      // A secure reset retires the old identity generation as well as its235+      // paired-device registry. Reusing the old record would let archived236+      // trust material identify this installation after the reset.237+      await rotateDeviceId();234238       return true;235239     } catch {236-      if (await hasLegacyTrustState(browserGenerationIo)) return false;237-      try {238-        await rotateDeviceId();239-        return true;240-      } catch {241-        return false;242-      }240+      return false;243241     }244242   },245243   deviceId: getOrCreateDeviceId,

view file

src/background/messages.test.ts+27 −2
@@ -1753,7 +1753,31 @@17531753       revokedByDeviceId: expect.any(String),17541754     });17551755   });1756+  it("UNPAIR_DEVICE still evicts a peer when the local identity needs migration", async () => {1757+    const vault = await import("./vault");1758+    await vault.createVault("correct-pass-12345");1759+    const identity = await import("./device-identity");1760+    await identity.getOrCreateDeviceId();1761+    delete storage.peach_local_identity_v4;1762+    delete sessionStorage.peach_local_identity_session;1763+    const { findPairedDeviceById, isDeviceRevoked, upsertPairedDevice } = await import("./paired-devices");1764+    await upsertPairedDevice({1765+      deviceId: "phone-1",1766+      deviceName: "June's Phone",1767+      deviceType: "android",1768+      longTermPublicKeyHex: "a".repeat(64),1769+      signingPublicKeyHex: "b".repeat(64),1770+      pairedAt: 1_700_000_000_000,1771+      trustSource: "direct",1772+    });17561773 1774+    const response = await dispatch({ type: "UNPAIR_DEVICE", payload: { deviceId: "phone-1" } });1775+1776+    expect(response).toMatchObject({ ok: true });1777+    expect(await findPairedDeviceById("phone-1")).toBeNull();1778+    expect(await isDeviceRevoked("phone-1", "a".repeat(64))).toBe(true);1779+  });1780+17571781   it("UNPAIR_DEVICE on an unknown id is a no-op (ok) and writes no revocation", async () => {17581782     const vault = await import("./vault");17591783     await vault.createVault("correct-pass-12345");@@ -1783,11 +1807,11 @@17831807     });17841808     expect(res).toMatchObject({ ok: false, error: "invalid_payload" });17851809   });1786-  it("SECURE_RESET archives legacy trust and reaches the new Sync anchor", async () => {1810+  it("SECURE_RESET archives legacy trust, rotates identity, and reaches the new Sync anchor", async () => {17871811     const vault = await import("./vault");17881812     await vault.createVault("correct-pass-12345");17891813     const identity = await import("./device-identity");1790-    await identity.getOrCreateDeviceId();1814+    const previousDeviceId = await identity.getOrCreateDeviceId();17911815     const { upsertPairedDevice } = await import("./paired-devices");17921816     await upsertPairedDevice({17931817       deviceId: "phone-1",@@ -1805,6 +1829,7 @@18051829     });18061830 18071831     expect(response).toMatchObject({ ok: true, data: { state: "V2_ANCHOR" } });1832+    expect(await identity.getOrCreateDeviceId()).not.toBe(previousDeviceId);18081833   });18091834 18101835   // The old "fresh direct re-pair clears the old revocation" test dispatched

view file

src/background/messages.ts+12 −4
@@ -1665,10 +1665,18 @@16651665         if (typeof deviceId !== "string" || !deviceId) {16661666           return { ok: false, error: "invalid_payload" };16671667         }1668-        // Removing a device REVOKES it rather than just dropping the local1669-        // row: a plain removal is undone the moment any other peer vouches it1670-        // back through a manifest, which is exactly what the relay makes easy.1671-        await revokeDeviceByIdV3(deviceId, await getOrCreateDeviceId());1668+        // A damaged legacy identity must not prevent the user from evicting a1669+        // peer locally. Sync is already blocked until the secure reset repairs1670+        // this installation, so the local issuer marker is sufficient for the1671+        // tombstone that will be cleared with the archived generation.1672+        let revokedByDeviceId: string;1673+        try {1674+          revokedByDeviceId = await getOrCreateDeviceId();1675+        } catch (error) {1676+          if (!(error instanceof Error) || error.message !== "identity_migration_required") throw error;1677+          revokedByDeviceId = "local";1678+        }1679+        await revokeDeviceByIdV3(deviceId, revokedByDeviceId);16721680         // PB-26-213: forget the secret now so no further dial can use it.16731681         // Removals that happen while locked leave orphans for the next16741682         // unlock's reconcile to prune.

view file

src/shared/release-notes.ts+8 −0
@@ -21,6 +21,14 @@2121 2222 export const RELEASES: PeachRelease[] = [2323   {24+    version: "5.2.47",25+    releasedAt: "2026-09-11",26+    notes: [{27+      heading: "Repair Sync reset and peer removal",28+      body: "Sync reset now creates the new device identity, and removing a peer still works when the old identity needs migration.",29+    }],30+  },31+  {2432     version: "5.2.46",2533     releasedAt: "2026-09-11",2634     importance: "breaking",

view file