Skip to main content

What Are Passkeys?

Passkeys use biometric authentication (Face ID, Touch ID, Windows Hello) to protect wallets. Instead of a PIN, a secure encryption key is derived from the biometric prompt. No passwords to remember, no PINs to leak. useCreateWallet supports three modes: On the web the passkey’s wrap key is the WebAuthn PRF output, derived on the device: Chipi never sees it and cannot recompute it. If the device or browser can’t produce PRF output, wallet creation stops with PRF_UNSUPPORTED instead of falling back to a server-derivable key (see When passkeys fail). A PIN backup is optional. It is a weaker secret: the backup ciphertext is stored by the backend, and a short PIN can be brute-forced offline against it. Add one only if your users need a recovery path that doesn’t depend on passkey sync.
The passkey is bound to your site’s hostname (rpId is window.location.hostname), so it unlocks the wallet only on that hostname. useCreateWallet names the passkey after the externalUserId you pass (user name externalUserId, display name "<externalUserId> - Wallet Key"), and that is what the OS passkey picker shows. Pass a value you are comfortable showing the user.

Platform Support

Installation

Create a wallet with passkey + PIN backup

Passkey is primary, PIN is backup:

What happens internally

  1. usePasskey: true → calls createWalletPasskey(externalUserId, externalUserId) → biometric prompt. It throws PrfUnsupportedError (code: "PRF_UNSUPPORTED") when the authenticator has no PRF; the hook rethrows it as Error("Passkey creation failed: …") with the same code.
  2. Returns encryptKey (passkey-derived) + credentialId + prfSupported
  3. Without encryptKey → passkey-only: private key encrypted with the passkey key → encryptedPrivateKey, authMethod: "passkey"
  4. With encryptKey (PIN) → dual-key mode:
    • Private key encrypted with passkey key → encryptedPrivateKey
    • Private key encrypted with PIN → encryptedPrivateKeyBackup
    • authMethod: "passkey+pin"
  5. credentialId and prfSupported are sent with the wallet, so getWallet returns them on any device
Source: chipi-react/src/hooks/useCreateWallet.ts

Unlock the wallet on another device

The passkey syncs to the user’s other devices through their platform account. What doesn’t sync is the browser’s localStorage, where createWalletPasskey keeps the credential metadata. getWalletEncryptKey() with no argument reads localStorage, so on a new device it throws "No wallet passkey found". Pass the values stored on the wallet row instead:
The signature is getWalletEncryptKey(credentialId?: string, options?: { prfSupported?: boolean }). prfSupported tells it the wallet was created with PRF, so it never falls back to a different key derivation when a browser returns no PRF output; it throws "Passkey PRF unavailable…" instead. The transaction hooks (useTransfer, useApprove, useCallAnyContract) accept the same credentialId param next to usePasskey: true.

Transfer with Passkey (Automatic PIN Fallback)

Transfer flow

  1. usePasskey: true → tries passkey authentication (biometric prompt)
  2. If passkey succeeds → decrypts encryptedPrivateKey with passkey key → signs tx
  3. If passkey fails (PRF unavailable, user cancelled, localStorage cleared):
    • Checks if wallet.encryptedPrivateKeyBackup exists
    • If yes → calls onPinRequired() → user enters PIN → decrypts backup key → signs tx
    • If no backup or no onPinRequired callback → throws descriptive error
Source: chipi-react/src/hooks/useTransfer.ts lines 65-110

UseTransferConfig

PIN-Only Mode (Legacy)

Omit usePasskey: true for the same PIN-only flow as before:

Migrate from PIN to Passkey

For existing PIN-only wallets:

Check Passkey Status

Expo (Mobile) Passkeys

On mobile, the same usePasskey: true flag works automatically — the Expo ChipiProvider injects a native biometric adapter.
On Expo the “passkey” is not a WebAuthn passkey: it is a random key stored in this device’s Keychain / Keystore (expo-secure-store) behind biometrics. The SDK keeps it only on this device, so a biometric-only Expo wallet is lost with the device. Keep the PIN backup on Expo.
If biometrics fail (user unenrolled, cancelled), the onPinRequired callback in useTransfer handles the fallback — same as web.

Hooks Reference

Security

  • Device-derived wrap key: On the web the passkey’s PRF output encrypts the private key. Without PRF, creation fails with PRF_UNSUPPORTED unless you pass requirePrf: false to createWalletPasskey, which makes the wallet custodial.
  • Optional dual-key wrapping: With a PIN, the same private key is also encrypted with the PIN. Either recovers it, and the PIN is the weaker of the two.
  • No silent fallback: If passkey was created with PRF, PBKDF2 fallback is blocked (prevents wrong-key decryption).
  • Credential metadata on the wallet row: credentialId and prfSupported are stored server-side and returned by getWallet. If localStorage is cleared or the user is on a new device, pass them to getWalletEncryptKey (or credentialId to the transaction hooks).
  • Hardware-backed: Keys in Secure Enclave (iOS), Android Keystore, or WebAuthn authenticator.