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
↓ peach-browser-9767a703.tar.gzatlas at this save
files changed5 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/crypto/peachPayload.test.ts+58 −0src/crypto/peachPayload.ts+21 −4manifest.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": {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",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": {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({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);