070c26097d42

preserve Chrome passkey registration flags in Firefox assertions (v5.3.3)

time
2026-10-08 15:38 UTC · 2 weeks ago
machine
Junes-MacBook-Pro.local
agent
gpt-6
session
repair firefox passkey sign-in
parent
a1cd10fe1734
5 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/passkey-firefox-proxy.test.ts+41 −0src/background/passkey.ts+5 −6
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "5.3.2",4+  "version": "5.3.3",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.3.2",5+  "version": "5.3.3",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.3.2",3+  "version": "5.3.3",44   "description": "Zero-knowledge password manager",55   "license": "GPL-3.0-only",66   "type": "module",

view file

src/background/passkey-firefox-proxy.test.ts+41 −0
@@ -434,6 +434,47 @@434434     ).resolves.toMatchObject({ ok: false, error: "no_credentials" });435435   });436436 437+  it.each([["chrome", true], ["firefox", false]] as const)("PASSKEY_GET preserves registration backup eligibility for a legacy %s passkey", async (browser, expectedBackupEligible) => {438+    const vault = await import("./vault");439+    await vault.createVault("correct-pass-12345");440+    const generated = await generatePasskeyKeypair(-7);441+    const credentialId = base64urlEncode(crypto.getRandomValues(new Uint8Array(32)));442+    const entry = await vault.addEntry({443+      type: "login",444+      title: "GitHub",445+      passkey: {446+        rpId: "github.com", rpName: "GitHub", credentialId,447+        publicKeyCose: base64urlEncode(generated.publicKeyCose),448+        privateKeyJwk: generated.privateKeyJwk,449+        userHandle: base64urlEncode(crypto.getRandomValues(new Uint8Array(16))),450+        userName: "june", userDisplayName: "June", algorithm: -7, signCount: 0, aaguid: "",451+        backupEligible: false, backupState: false, createdAt: 1, browser,452+      },453+    });454+    const challenge = "challenge-base64url";455+    const origin = "https://github.com";456+    const clientDataJSON = Array.from(new TextEncoder().encode(457+      JSON.stringify({ type: "webauthn.get", challenge, origin, crossOrigin: false })458+    ));459+460+    const resultPromise = dispatch({461+      type: "PASSKEY_GET",462+      payload: { rpId: "github.com", challenge, origin, clientDataJSON, allowCredentials: [{ id: credentialId, type: "public-key" }] },463+    });464+    const passkey = await import("./passkey");465+    await vi.waitFor(async () => {466+      expect(await passkey.getPendingPasskeyRequest()).not.toBeNull();467+    });468+    const pending = await passkey.getPendingPasskeyRequest();469+    await passkey.resolvePendingPasskeyRequest(pending!.requestId, true);470+    const result = await resultPromise;471+    expect(result.ok).toBe(true);472+    const assertion = (result as unknown as { credential: { authenticatorData: string } }).credential;473+    const authenticatorData = Uint8Array.from(atob(assertion.authenticatorData.replace(/-/g, "+").replace(/_/g, "/")), (char) => char.charCodeAt(0));474+    expect(Boolean(authenticatorData[32]! & 0x08)).toBe(expectedBackupEligible);475+    expect(vault.getEntries().find((candidate) => candidate.id === entry.id)!.passkey!.backupEligible).toBe(expectedBackupEligible);476+  });477+437478   it("PASSKEY_CREATE registers a passkey and increments signCount on PASSKEY_GET", async () => {438479     const vault = await import("./vault");439480     await vault.createVault("correct-pass-12345");

view file

src/background/passkey.ts+5 −6
@@ -1308,12 +1308,10 @@13081308   if (!approved) return { ok: false, error: "user_cancelled" };13091309 13101310   const nextSignCount = credential.signCount + 1;1311-  // 0x05 = UP|UV. backup bits track whatever was set at registration time1312-  // (consistent with the Chrome path).1313-  const flags =1314-    0x05 |1315-    (credential.backupEligible ? 0x08 : 0) |1316-    (credential.backupState ? 0x10 : 0);1311+  // Legacy Chrome registrations used BE=1 even when the stored flag was false.1312+  // Legacy Firefox registrations used BE=0, so retain their recorded value.1313+  const backupEligible = credential.browser === "chrome" ? true : credential.backupEligible;1314+  const flags = 0x05 | (backupEligible ? 0x08 : 0) | (credential.backupState ? 0x10 : 0);13171315   const authenticatorData = await buildAuthenticatorData({13181316     rpId: credential.rpId,13191317     flags,@@ -1337,6 +1335,7 @@13371335   await vault.updateEntry(match.id, {13381336     passkey: {13391337       ...credential,1338+      backupEligible,13401339       signCount: nextSignCount,13411340       lastUsedAt: Date.now(),13421341     },

view file