Skip to content

feat(settings): add the passkey wrap creation hook - #21187

Closed
vpomerleau wants to merge 1 commit into
mainfrom
FXA-14425
Closed

feat(settings): add the passkey wrap creation hook#21187
vpomerleau wants to merge 1 commit into
mainfrom
FXA-14425

Conversation

@vpomerleau

@vpomerleau vpomerleau commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Because

  • Passwordless Sync must seal kB to a passkey's PRF output before a wrap can be stored.
  • The store is create-only, so a bad or duplicated envelope wedges the credential until
    the passkey is re-enrolled.

This pull request

  • Adds lib/passkeys/wrap/: createPasskeyWrapFlow seals kB into a wrap envelope and
    stores it under an mfa:passkey proof; usePasskeyWrapCreation wraps it in React state.
  • Reads the account and credential from the proof's sub/cid before touching key
    material, so the sealing context always matches what the server files the wrap under.
  • Reopens each envelope before storing it, and rejects missing, wrong-width or spent key
    material.
  • Holds a sealed-but-unstored envelope in a store shared across instances, so a retry —
    including after a remount — re-sends identical bytes.
  • Maps server errnos to distinct failure reasons and evicts a rejected proof from
    JwtTokenCache.
  • Zeroes kB and the PRF output once sealed.

Issue that this pull request solves

Closes: FXA-14425

Checklist

Put an x in the boxes that apply

  • My commit is GPG signed.
  • If applicable, I have modified or added tests which pass locally.
  • I have added necessary documentation (if appropriate).
  • I have verified that my changes render correctly in RTL (if appropriate).
  • I have manually reviewed all AI generated code.

How to review (Optional)

  • Key files/areas to focus on: wrap/interfaces.ts (the failure-reason union and
    result types) and wrap/creation.ts (the flow). The hook is a 60-line state wrapper.
    Tests and fixtures are ~65% of the diff.
  • Suggested review order: interfaces.tscreation.ts top to bottom →
    use-passkey-wrap-creation.tscreation.test.ts, the retries and proof claims
    blocks.
  • Risky or complex parts: the create-only store means a mis-sealed or re-sealed
    envelope wedges the credential permanently, so three guards do the heavy lifting: the
    reopen-before-store check, the held envelope shared across instances via
    passkeyWrapStore, and the cid/sub read from the proof before any key material is
    touched. kB/prfOut zeroing on every path is the other thing worth tracing.

Screenshots (Optional)

Please attach the screenshots of the changes made in case of change in user interface.

Other information (Optional)

Not exported from lib/hooks/index.ts or lib/passkeys/index.ts: the flow pulls in the
HPKE suite, which builds a CipherSuite at module scope. Consumers deep-import
lib/passkeys/wrap, as pages/Signin does with usePasskeySignIn.

This comment was marked as outdated.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Same-tick calls can corrupt hook state because early flow results never set flow.inFlight.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread packages/fxa-settings/src/lib/passkeys/wrap/interfaces.ts Outdated
Because:
- Passwordless Sync needs kB sealed to a passkey's PRF output before a
  wrap can be stored.
- The wrap store is create-only, so a mis-sealed or duplicated envelope
  wedges the credential until the passkey is deleted.

This commit:
- Adds createPasskeyWrapFlow, sealing kB into a wrap envelope bound to
  the account and credential the mfa:passkey proof names, and
  usePasskeyWrapCreation as its React state wrapper.
- Reopens the envelope before storing it, catching platform crypto that
  seals what it cannot unseal.
- Refuses spent, zeroed or wrong-width key material, and holds a sealed
  envelope in a store shared across instances so a retry re-sends
  identical bytes.
- Maps server errnos to distinct failure reasons and drops a rejected
  proof from the JWT cache.
- Zeroes kB and the PRF output once sealed, on every path that reads them.

Closes #FXA-14425

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A mismatched-key retry discards the only byte-identical envelope and can wedge a credential after an ambiguous successful POST.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 1
  • Review effort level: Balanced

'passkey_not_found',
'wrap_conflict',
'feature_disabled',
'key_changed',
@vpomerleau

Copy link
Copy Markdown
Contributor Author

Working on a shorter version of this PR - will open as a new PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants