Goal: Orchestrate PRF-capable passkey migration from Apple Passwords → Bitwarden iOS → Bitwarden Android → Nuri wallet recovery (issue #68).
Decisions:
- Extension state is a single opaque encrypted string (JSON of Fido2ExtensionStateView), not individual encrypted fields. Server treats it as opaque payload.
- CXF import maps fido2_extensions.hmac_credentials to Fido2ExtensionStateView JSON. Export reverses this.
- JSON export deliberately omits extension_state (ADR §6: no PRF seed disclosure in default export).
- Algorithm validation: ES256 (-7) supported, RS256 (-257) and others rejected explicitly.
- deny_unknown_fields removed from Fido2Credential for forward compatibility with old clients.
- HMAC state bridged bidirectionally between Fido2ExtensionStateView and passkey-rs StoredHmacSecret.
State:
- 8 issues implemented, all in status:review:
- #21 + #26 → sdk-internal PR #1 (extension model + algorithm validation)
- #20 → server PR #1 (persist encrypted extension state)
- #22 → sdk-internal PR #2 (CXF import preserve extensions)
- #60 → sdk-internal PR #3 (blob round-trip tests)
- #61 → sdk-internal PR #4 (bridge HMAC state to passkey-rs)
- #62 → sdk-internal PR #5 (on-demand PRF evaluation on get_assertion)
- #63 → sdk-internal PR #6 (UV seed selection + fail-closed PRF semantics)
- #17 promoted to status:ready (API model already updated, binding regeneration requires build)
- All PRs blocked on external review
- Safety classifier blocks cargo check and agent launches; coordinator implemented everything directly
- Verified passkey-rs API: HmacSecretConfig::new_without_uv() (not new_with_uv_and_non_uv), no enable_on_get_assertion() method, PRF on get_assertion is automatic when config present
Next steps:
- External review of all 7 PRs (6 sdk-internal + 1 server)
- After #21, #26 merge: regenerate Swift/Kotlin bindings for #17
- After #20, #21, #22, #60, #61, #62, #63 merge: promote #31 (gate: prove portability pipeline)
- Build verification once classifier allows cargo commands
Open questions:
- Build verification pending — safety classifier blocks cargo check
- Nuri-expo PRs (#859, #860, #861) blocked by branch protection requiring external review
- Whether Fido2Extensions and HmacCredentials types from credential-exchange-format crate match the assumed structure (could not verify crate source directly)
Goal: Emin ran the real-device GPM passkey export → Bitwarden Android import as the first step of the GPM → Android → iOS same-passkey-same-PRF proof.
Decisions:
- Import abort root-caused to Google exporting Chrome-epoch-zero dates as negative Unix time (-11644473600), failing CXF u64 types.
- Fix placed in the fork at
parse_cxf(sanitize negatives → "no date set"), not in the externalcredential-exchange-formatcrate. Upstream filing still open. - Issue tracked as #86.
State: sdk-internal@0511e7ba pushed, 123 exporter tests green. Fix not yet in the Android dev build (needs SDK package republish + repin).
Next steps:
- Republish fork Android SDK package, repin nuri-com/android, rebuild dev, retry import.
- Log raw CXF payload: does GPM export
fido2Extensions.hmacCredentials? If no → PRF continuity via GPM impossible, re-scope track. - If yes: sync → install nuri iOS build (#67) → fixed-salt PRF comparison Android vs iOS.
Open questions:
- Does GPM export hmacCredentials at all? (make-or-break for the whole GPM track)
- Upstream issue against bitwarden/sdk-internal + credential-exchange-format crate not yet filed.
Goal: Match 1Password on pwa.nuri.com: Bitwarden-created Android passkeys must register as PRF-capable.
Decisions:
- Root cause is Android response mapping, not SDK cryptography: sdk-internal already returns makeCredential PRF output, but Android serialized only credProps.
- Map
clientExtensionResults.prf.enabledand optional results into Credential Manager registration JSON.
State: nuri-com/android@aa1e88111 pushed; targeted CI test and dev APK build passed; APK installed on Emin's S21.
Next steps:
- Delete/revoke the old non-PRF Bitwarden credential on pwa.nuri.com.
- Create a fresh credential with Bitwarden dev and verify wallet creation/recovery.
- Then revisit imported GPM credentials separately; GPM payload observed without hmacCredentials.
Open questions:
- Does pwa.nuri.com now accept the fresh Bitwarden credential and receive stable PRF output?
Goal: Make GPM → Bitwarden Android import retain the same passkey PRF, matching 1Password and enabling Android/iOS use.
Decisions:
- GPM does export PRF state; blob-only errors from unrelated credentials did not prove otherwise.
- Root cause was Android's legacy structured upload: official Cloud retained the passkey but dropped fork-only extensionState during forced sync.
- Match iOS: re-encrypt imported portable passkeys through SDK blob encryption, require Cipher.data, retain only the legacy name required by /ciphers/import.
State: nuri-com/android@bcf5ebaff pushed; targeted CI tests + APK build passed; APK installed on Emin's S21.
Next steps:
- Delete the old Bitwarden-imported Nuri passkey (its PRF state is already lost).
- Re-import fresh from GPM with the installed build.
- Test pwa.nuri.com on Android, then sync/install iOS and compare the same wallet identity.
Open questions:
- Real-device proof that the re-imported Nuri passkey now returns the exact original GPM PRF output.
Goal: Import real Google Password Manager credentials into Bitwarden, preserve PRF state through official Bitwarden Cloud, and prove the same Nuri wallet through Google and Bitwarden on Android before continuing to iOS.
Decisions:
- Portable PRF state travels only inside authenticated opaque
Cipher.data; official Cloud must not receive fork-only structured extension fields. - V1-account portable imports explicitly request blob encryption instead of relying on the account-wide default gate.
- Credential Manager discovery uses loaded FIDO2 metadata and decrypts only the selected cipher, avoiding Android's roughly three-second provider deadline.
- The proof compares SHA-256 fingerprints and public wallet identity; raw credential IDs, PRF outputs, seeds, CXF payloads, and private keys remain secret.
- Google source passkeys must not be deleted. Only the disposable Bitwarden test vault may be cleaned before a single fresh import.
- Timestamp/import bug #86 is complete; the remaining device comparison is isolated in #87.
State: SDK heads 0511e7ba, b1589e48, and c4b2ae57 plus Android heads aa1e88111, bcf5ebaff, 6a4b8b1a7, and 2f5c485b8 are merged on fork mains. Android run 29912811796 is green and its APK was installed during the test. A real GPM transfer of 1,329 passwords and 77 passkeys completed. Bitwarden answered Credential Manager before timeout. Google local assertion reached biometric verification, but no baseline was completed before the device disconnected. The Bitwarden test vault was not emptied and still contains roughly 2,812 repeated-import logins.
Next steps:
- Reconnect and verify the Samsung
SM-G991Bover ADB. - Install the secret-safe Chrome proof hook and finish the Google baseline with one known named Nuri passkey.
- Empty only the Bitwarden test vault, including trash; keep GPM unchanged.
- Import once, sync/reload, make Bitwarden Dev the sole local provider, and repeat with the same named passkey.
- Require exact credential fingerprint, PRF fingerprint, and public wallet match; then repeat through iOS.
Open questions:
- Does the clean Bitwarden import reproduce the exact Google PRF fingerprint and Nuri wallet identity?
- Does the same opaque Cloud item remain usable in the pinned iOS build?
- Is production duplicate-credential handling needed after a clean single-import proof?