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
↓ peach-browser-2f80eea2.tar.gzatlas at this save
files changed6 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 −0manifest.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": {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",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",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> {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 livesrc/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: [{