19da2c5da38d

fix(data): normalizeEntry preserves passwordChangedAt — aged-password health uses correct date across lock/unlock (v3.6.111)

time
2026-05-28 20:06 UTC · 5 months ago
machine
koh2
parent
66b318659107
5 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/vault.test.ts+32 −0src/background/vault.ts+5 −0
manifest.firefox.json+1 −1
@@ -2,7 +2,7 @@22   "_comment_audio_capture": "Firefox does not honor the Chrome 'audioCapture' optional_permission — getUserMedia in WebExtensions is gated solely by the runtime permission prompt. Microphone access for chirp pairing works from the popup under the existing '<all_urls>' host permission; no manifest entry is required or accepted here.",33   "manifest_version": 3,44   "name": "Peach Password Manager",5-  "version": "3.6.110",5+  "version": "3.6.111",66   "description": "Zero-knowledge password manager",77   "browser_specific_settings": {88     "gecko": {

view file

manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "3.6.110",4+  "version": "3.6.111",55   "description": "Zero-knowledge password manager",66   "action": {77     "default_popup": "src/popup/index.html",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "3.6.110",3+  "version": "3.6.111",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

src/background/vault.test.ts+32 −0
@@ -900,4 +900,36 @@900900       "https://staging.example.com",901901     ]);902902   });903+904+  it("normalizeEntry preserves passwordChangedAt through lock/unlock", async () => {905+    const vault = await import("./vault");906+    await vault.createVault("pwchanged-preserve-pass");907+    const entry = await vault.addEntry({908+      title: "Aged",909+      username: "june",910+      password: "secret",911+      url: "https://aged.test",912+    });913+    // addEntry stamps passwordChangedAt at creation; capture it and prove it914+    // survives the encode/decode round-trip unchanged (was dropped pre-fix).915+    const ts = entry.passwordChangedAt;916+    expect(typeof ts).toBe("number");917+    vault.lock();918+919+    expect(await vault.unlock("pwchanged-preserve-pass")).toBe(true);920+    const restored = vault.getEntries().find((e) => e.id === entry.id);921+    expect(restored?.passwordChangedAt).toBe(ts);922+  });923+924+  it("normalizeEntry handles entries without passwordChangedAt", async () => {925+    const vault = await import("./vault");926+    await vault.createVault("pwchanged-absent-pass");927+    // A note carries no password and therefore no passwordChangedAt stamp.928+    const note = await vault.addEntry({ type: "note", title: "Memo", notes: "x" });929+    vault.lock();930+931+    expect(await vault.unlock("pwchanged-absent-pass")).toBe(true);932+    const restored = vault.getEntries().find((e) => e.id === note.id);933+    expect(restored?.passwordChangedAt).toBeUndefined();934+  });903935 });

view file

src/background/vault.ts+5 −0
@@ -2071,6 +2071,11 @@20712071     totp: entry.totp,20722072     notes: entry.notes,20732073     breach: entry.breach,2074+    // Preserve the password-age clock across lock/unlock. Without this the2075+    // field is dropped on every decode and the advanced-health "aged password"2076+    // signal silently resets to createdAt on each unlock (SEV-2-A).2077+    passwordChangedAt:2078+      "passwordChangedAt" in entry ? entry.passwordChangedAt : undefined,20742079     canary: "canary" in entry ? entry.canary : undefined,20752080     passkey: "passkey" in entry ? entry.passkey : undefined,20762081     createdAt,

view file