2f80eea29ae0

make Sync identity cache best effort during reset (v5.2.49)

time
2026-09-11 19:23 UTC · 1 month ago
machine
koh2
agent
camel-stream/auto
session
fix secure reset completion
parent
a7728642a731
6 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/device-identity.ts+6 −1src/background/messages.test.ts+60 −0src/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.48",4+  "version": "5.2.49",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.48",5+  "version": "5.2.49",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.48",3+  "version": "5.2.49",44   "description": "Zero-knowledge password manager",55   "license": "GPL-3.0-only",66   "type": "module",

view file

src/background/device-identity.ts+6 −1
@@ -207,7 +207,12 @@207207 }208208 209209 async function cacheSet(record: IdentityRecord): Promise<void> {210-  await chrome.storage.session?.set({ [SESSION_CACHE_KEY]: record });210+  try {211+    await chrome.storage.session?.set({ [SESSION_CACHE_KEY]: record });212+  } catch {213+    // The durable local envelope is authoritative; session cache is only a214+    // restart optimization and must not block identity creation or rotation.215+  }211216 }212217 213218 async function clearCache(): Promise<void> {

view file

src/background/messages.test.ts+60 −0
@@ -1858,7 +1858,67 @@18581858     expect(vault.isUnlocked()).toBe(true);18591859     expect(await identity.getOrCreateDeviceId()).not.toBe(previousDeviceId);18601860   });1861+  it("SECURE_RESET completes after fresh unlock when the locked spool hits identity migration", async () => {1862+    const vault = await import("./vault");1863+    const syncAdapter = await import("./sync-vault-adapter");1864+    vault.setLockedSyncHooks({1865+      refresh: syncAdapter.refreshLockedSyncSpool,1866+      afterUnlock: syncAdapter.drainAndRefreshLockedSyncSpool,1867+    });1868+    await vault.createVault("correct-pass-12345");1869+    const identity = await import("./device-identity");1870+    const previousDeviceId = await identity.getOrCreateDeviceId();1871+    const { upsertPairedDevice } = await import("./paired-devices");1872+    await upsertPairedDevice({1873+      deviceId: "phone-1",1874+      deviceName: "June's Phone",1875+      deviceType: "android",1876+      longTermPublicKeyHex: "a".repeat(64),1877+      signingPublicKeyHex: "b".repeat(64),1878+      pairedAt: 1_700_000_000_000,1879+      trustSource: "direct",1880+    });1881+    await vault.addEntry({ type: "login", title: "spooled", password: "secret" });1882+    await syncAdapter.refreshLockedSyncSpool();1883+    await vault.lock();1884+    const { createLockedSyncVaultAdapter } = await import("./locked-sync-spool");1885+    const lockedAdapter = await createLockedSyncVaultAdapter();1886+    expect(lockedAdapter).not.toBeNull();1887+    await lockedAdapter!.applyEntries({ remote: "AA==" }, { remote: Date.now() + 1 });1888+    delete storage.peach_local_identity_v4;1889+    delete sessionStorage.peach_local_identity_session;1890+    const sessionSet = chrome.storage.session.set as unknown as ReturnType<typeof vi.fn>;1891+    sessionSet.mockImplementation(async (next: Record<string, unknown>) => {1892+      if (Object.hasOwn(next, "peach_local_identity_session")) {1893+        throw new Error("QUOTA_BYTES");1894+      }1895+      Object.assign(sessionStorage, next);1896+    });18611897 1898+    const unlocked = await dispatch({1899+      type: "UNLOCK",1900+      payload: { password: "correct-pass-12345" },1901+    });1902+    const pairSecrets = await import("./pair-secrets");1903+    const root = Uint8Array.from((vault.getRootKeyHex() ?? "").match(/../g)!, (part) => Number.parseInt(part, 16));1904+    await pairSecrets.setPairSecret("phone-1", "c".repeat(64), root);1905+    root.fill(0);1906+    expect(storage.peach_pair_secrets_v1).toBeDefined();1907+    expect(sessionStorage.peach_pair_secrets_session).toBeDefined();1908+    expect(unlocked).toMatchObject({ ok: true });1909+1910+    const response = await dispatch({1911+      type: "SECURE_RESET",1912+      payload: { password: "correct-pass-12345" },1913+    });1914+1915+    expect(response).toMatchObject({ ok: true, data: { state: "V2_ANCHOR" } });1916+    expect(storage.peach_local_identity_v4).toBeDefined();1917+    expect(storage.peach_paired_devices_v3).toBeUndefined();1918+    expect(storage.peach_pair_secrets_v1).toBeUndefined();1919+    expect(sessionStorage.peach_pair_secrets_session).toBeUndefined();1920+    expect(await identity.getOrCreateDeviceId()).not.toBe(previousDeviceId);1921+  });18621922   // The old "fresh direct re-pair clears the old revocation" test dispatched18631923   // COMPLETE_PAIRING, a handler deleted on 2026-08-04 (no legitimate senders;18641924   // adoption runs in-background via relay-adoption-bg.ts). The live

view file

src/shared/release-notes.ts+8 −0
@@ -21,6 +21,14 @@2121 2222 export const RELEASES: PeachRelease[] = [2323   {24+    version: "5.2.49",25+    releasedAt: "2026-09-11",26+    notes: [{27+      heading: "Make Sync reset resilient",28+      body: "Sync reset no longer fails when the browser cannot refresh its session-only identity cache; the durable identity remains authoritative.",29+    }],30+  },31+  {2432     version: "5.2.48",2533     releasedAt: "2026-09-11",2634     notes: [{

view file