9767a70364ea

fix(data): PeachScript passkey entry encodes all login fields — totp, notes, tags, email, canary, additionalUrls, passwordChangedAt, breach preserved on export; backward-compatible via JSON blob (v3.6.121)

time
2026-05-28 20:25 UTC · 5 months ago
machine
koh2
parent
2b15bb2c1fdd
5 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/crypto/peachPayload.test.ts+58 −0src/crypto/peachPayload.ts+21 −4
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.120",5+  "version": "3.6.121",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.120",4+  "version": "3.6.121",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.120",3+  "version": "3.6.121",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

src/crypto/peachPayload.test.ts+58 −0
@@ -160,6 +160,64 @@160160     expect(gotPk.backupState).toBe(false);161161   });162162 163+  test("passkey-bearing login entry round-trips ALL login fields (SEV-1-B)", async () => {164+    const canary: Canary = { webhookUrl: "https://example.com/canary", label: "alert" };165+    const entry: VaultEntry = {166+      id: crypto.randomUUID(),167+      type: "login",168+      title: "Google",169+      username: "[email protected]",170+      email: "[email protected]",171+      password: "also-has-a-password",172+      url: "https://google.com",173+      additionalUrls: ["https://accounts.google.com"],174+      totp: "JBSWY3DPEHPK3PXP",175+      notes: "backup codes: 1234 5678",176+      tags: ["work", "important"],177+      canary,178+      passwordChangedAt: 1700000000000,179+      breach: 2,180+      passkey: {181+        rpId: "google.com",182+        rpName: "Google",183+        credentialId: "dGVzdC1pZA",184+        publicKeyCose: "a40102032620",185+        privateKeyJwk: { kty: "EC", crv: "P-256", x: "x", y: "y", d: "d" },186+        userHandle: "dGVzdA",187+        userName: "[email protected]",188+        userDisplayName: "Ada Lovelace",189+        algorithm: -7,190+        signCount: 42,191+        aaguid: "AAAAAAAAAAAAAAAAAAAAAA",192+        backupEligible: true,193+        backupState: false,194+        createdAt: 1690000000000,195+      },196+      createdAt: 1690000000000,197+      modifiedAt: 1700000000000,198+    };199+200+    const env = await buildPayload({ entries: [entry], settings: {}, passphrase: PASS });201+    const out = await parsePayload(env, PASS);202+    const got = out.entries[0]!;203+204+    // Login fields that were silently dropped on export before this fix.205+    expect(got.totp).toBe(entry.totp);206+    expect(got.notes).toBe(entry.notes);207+    expect(got.tags).toEqual(entry.tags);208+    expect(got.email).toBe(entry.email);209+    expect(got.additionalUrls).toEqual(entry.additionalUrls);210+    expect(got.canary).toEqual(canary);211+    expect(got.passwordChangedAt).toBe(entry.passwordChangedAt);212+    expect(got.breach).toBe(2);213+    expect(got.password).toBe(entry.password);214+    // The passkey credential still round-trips (decoded as the flat legacy215+    // shape; vault import re-nests it into the passkey sub-object).216+    expect(got.credentialId).toBe("dGVzdC1pZA");217+    expect(got.signCount).toBe(42);218+    expect(got.privateKeyJwk).toEqual(entry.passkey!.privateKeyJwk);219+  });220+163221   test("string table deduplicates shared domains", async () => {164222     const entries = Array.from({ length: 20 }, (_, i) =>165223       loginEntry({

view file

src/crypto/peachPayload.ts+21 −4
@@ -569,10 +569,27 @@569569       backupEligible:   pk ? pk.backupEligible   : flat["backupEligible"],570570       backupState:      pk ? pk.backupState      : flat["backupState"],571571       lastUsedAt:       pk ? pk.lastUsedAt       : flat["lastUsedAt"],572-      // Preserve login fields for passkey-bearing login entries.573-      ...(pk && entry.password  !== undefined ? { password:  entry.password  } : {}),574-      ...(pk && entry.url       !== undefined ? { url:       entry.url       } : {}),575-      ...(pk && entry.username  !== undefined ? { username:  entry.username  } : {}),572+      // Preserve the FULL login field set for passkey-bearing login entries.573+      // A v2.11+ passkey hangs off a normal login (type:"login" + passkey574+      // sub-object), so it can carry a password, TOTP seed, notes, tags, email,575+      // canary, additional URLs, and the password-age stamp — all of which were576+      // silently dropped on export before this (SEV-1-B). The blob is JSON, so577+      // adding keys is backward-compatible: old decoders ignore unknown keys578+      // and old exports simply lack them. Only emit these when `pk` is set;579+      // pre-v2.11 flat passkey entries had no login fields to preserve.580+      ...(pk ? {581+        ...(entry.password          !== undefined ? { password:          entry.password } : {}),582+        ...(entry.url               !== undefined ? { url:               entry.url } : {}),583+        ...(entry.username          !== undefined ? { username:          entry.username } : {}),584+        ...(entry.email             !== undefined ? { email:             entry.email } : {}),585+        ...(entry.totp              !== undefined ? { totp:              entry.totp } : {}),586+        ...(entry.notes             !== undefined ? { notes:             entry.notes } : {}),587+        ...(entry.tags              !== undefined ? { tags:              entry.tags } : {}),588+        ...(entry.canary            !== undefined ? { canary:            entry.canary } : {}),589+        ...(entry.passwordChangedAt !== undefined ? { passwordChangedAt: entry.passwordChangedAt } : {}),590+        ...(entry.breach            !== undefined ? { breach:            entry.breach } : {}),591+        ...(entry.additionalUrls    !== undefined ? { additionalUrls:    entry.additionalUrls } : {}),592+      } : {}),576593     });577594     writer.u32(blob.length);578595     writer.bytes(blob);

view file