0b702b354fb3

fix passkey backup eligibility flags across Chrome and Firefox ceremonies with regression coverage (PB-26-318, v5.0.87)

time
2026-09-04 00:25 UTC · 1 month ago
machine
koh2
agent
claude-sonnet-4-6
session
PB-26-318 passkey backup flag
parent
6d01667c8d96
6 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/passkey-chrome-ceremony.test.ts+5 −0src/background/passkey-firefox-proxy.test.ts+3 −0src/background/passkey.ts+5 −5
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "5.0.86",4+  "version": "5.0.87",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.0.86",5+  "version": "5.0.87",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.0.86",3+  "version": "5.0.87",44   "description": "Zero-knowledge password manager",55   "license": "GPL-3.0-only",66   "type": "module",

view file

src/background/passkey-chrome-ceremony.test.ts+5 −0
@@ -451,8 +451,13 @@451451     const stored = vault.getEntries().find((e) => e.passkey?.rpId === "github.com");452452     expect(stored).toBeDefined();453453     expect(stored!.passkey!.browser).toBe("chrome");454+    expect(stored!.passkey!.backupEligible).toBe(true);454455     expect(stored!.passkey!.signCount).toBe(0);455456     expect(stored!.passkey!.privateKeyJwk).toBeDefined();457+458+    const createResponse = JSON.parse(completes.create.find((c) => c.requestId === 124)!.responseJson!);459+    const authenticatorData = Uint8Array.from(atob(createResponse.response.authenticatorData.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(createResponse.response.authenticatorData.length / 4) * 4, "=")), (char) => char.charCodeAt(0));460+    expect(authenticatorData[32]! & 0x08).toBe(0x08);456461   });457462 458463   it("create attaches the passkey to an existing login entry for the same domain instead of creating a new one", async () => {

view file

src/background/passkey-firefox-proxy.test.ts+3 −0
@@ -470,6 +470,7 @@470470     expect(stored).toBeDefined();471471     expect(stored!.passkey!.signCount).toBe(0);472472     expect(stored!.passkey!.browser).toBe("firefox");473+    expect(stored!.passkey!.backupEligible).toBe(true);473474 474475     const clientDataGet = Array.from(new TextEncoder().encode(475476       JSON.stringify({ type: "webauthn.get", challenge, origin, crossOrigin: false })@@ -492,6 +493,8 @@492493     expect(assertion.type).toBe("get");493494     expect(typeof assertion.signature).toBe("string");494495     expect(typeof assertion.authenticatorData).toBe("string");496+    const authenticatorData = Uint8Array.from(atob(assertion.authenticatorData.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(assertion.authenticatorData.length / 4) * 4, "=")), (char) => char.charCodeAt(0));497+    expect(authenticatorData[32]! & 0x08).toBe(0x08);495498 496499     const after = vault.getEntries().find((e) => e.passkey?.rpId === "github.com");497500     expect(after!.passkey!.signCount).toBe(1);

view file

src/background/passkey.ts+5 −5
@@ -532,7 +532,7 @@532532     algorithm,533533     signCount: 0,534534     aaguid: base64urlEncode(new Uint8Array(16)),535-    backupEligible: false,535+    backupEligible: true,536536     backupState: false,537537     browser: "chrome",538538     transports: ["internal"],@@ -1199,11 +1199,11 @@11991199   const { bytes: credentialIdBytes, base64url: credentialId } = generateCredentialId();12001200   const { publicKeyCose, publicKeySpki, privateKeyJwk } = await generatePasskeyKeypair(algorithm);12011201 1202-  // 0x45 = UP|UV|AT  (user-present + user-verified + attested-credential-data).1203-  // Matches Chrome's create flag set in handleCreateRequest above.1202+  // 0x4d = UP|UV|BE|AT. Peach passkeys are synced, so BE is true on both1203+  // registration paths and on later assertions.12041204   const authenticatorData = await buildAuthenticatorData({12051205     rpId: payload.rpId,1206-    flags: 0x45,1206+    flags: 0x4d,12071207     signCount: 0,12081208     credentialId: credentialIdBytes,12091209     publicKeyCose,@@ -1222,7 +1222,7 @@12221222     algorithm,12231223     signCount: 0,12241224     aaguid: base64urlEncode(new Uint8Array(16)),1225-    backupEligible: false,1225+    backupEligible: true,12261226     backupState: false,12271227     browser: "firefox",12281228     transports: ["internal"],

view file