Passkeys
Add a passkey after sign-up so a new device can open the wallet. It carries an encrypted copy of the wallet's key, on Starknet, Solana, and Stellar alike. Signing up never asks for one.
Signing up with Google / Apple / email never asks for a passkey. The wallet
exists without one. The app decides when to add it (enrollPasskeyDefault), and
from then on a new device asks for it when it needs the wallet's key.
On Starknet, Solana, and native Stellar the passkey holds an encrypted copy of the wallet's key (the MasterDEK). Cavos stores the ciphertext; only the passkey can open it. It can spend: anyone with a synced passkey that was added can open the wallet. This is not 2FA.
deviceApproval: "passkey" is one chain. Use deviceApproval: "enclave"
when Starknet, Solana, and Stellar share a login.
Default: secureStep is 'off'. The built-in modal will not show "Secure
your account" after signup. Call enrollPasskeyDefault() from your own UI
(settings, first transaction, a banner). Set secureStep: 'optional' or
'required' only if you want that screen in the modal.
Add a passkey (React)
import { CavosProvider } from '@cavos/kit/react';
<CavosProvider
config={{
appId,
chains: ["solana"],
network: "testnet",
appSalt: "my-app",
deviceApproval: "passkey", // single chain only
}}
modal={{
appName: "My App",
secureStep: "off", // default — sign-up never prompts
}}
>
{children}
</CavosProvider>const { enrollPasskeyDefault, walletStatus } = useCavos();
// From a device that holds the wallet, when the user asks for it:
await enrollPasskeyDefault();
walletStatus.hasPasskey; // true once a passkey can restore this walletsecureStep | Behavior after a new account |
|---|---|
'off' (default) | No passkey screen. Call enrollPasskeyDefault() yourself. |
'optional' | Modal "Secure your account" with Skip. |
'required' | Same screen without Skip. |
enrollPasskeyDefault() resolves only once the passkey can restore the wallet.
A user can add more than one (say, an Apple and a Google passkey); any of them
works on a new device.
On a new device
The user signs in as usual. If a passkey was added, the vault shows "Verify
it's you" and asks for it. The wallet opens with the same address. If no
passkey was added, nothing is asked and the device stays signed in without the
key (walletStatus.needsDeviceApproval).
If the user dismissed the prompt, ask again with:
const { approveDeviceWithPasskey, walletStatus } = useCavos();
if (walletStatus.needsDeviceApproval && walletStatus.hasPasskey) {
await approveDeviceWithPasskey();
}A send from a device that is not authorized yet opens the passkey screen and rejects with "Verify it's you with your passkey to use this device, then try again."
How the copy is protected
The wallet's key comes from a random 32-byte MasterDEK. Adding a passkey stores a copy of it encrypted under a key only the passkey can produce, its WebAuthn PRF output:
KEK = HKDF-SHA256(passkey PRF, "cavos-passkey-dek-wrap-v1")
wrap = 0x01 || nonce || AES-256-GCM(KEK, DEK, aad = "appId:userId")- Cavos stores the wrap, one per passkey. The PRF never leaves the user's device, so Cavos cannot open it.
- On a new device the vault downloads the wraps, asks the passkey for its PRF, decrypts locally, and checks the DEK derives the registered address before using it. A wrong or tampered copy is refused.
- The passkey is created in the vault, so it belongs to
vault.cavos.xyz, not to your domain.
A synced passkey that was added is a way into the wallet. Frame it as "Add a passkey so you can open this wallet on any device", not as 2FA. Use the enclave if you want Google/Apple restore without giving a synced credential that power.
On Starknet and Solana there is no on-chain approver and no enrollPasskey on the
wallet; use enrollPasskeyDefault() from the hook.
Prerequisites
- A PRF-capable passkey provider: iCloud Keychain, Google Password Manager, Windows Hello, 1Password and most others. Providers without PRF cannot add a passkey.
- HTTPS or
http://localhost. A LAN IP fails WebAuthn. Use a tunnel for phones. - Gate on
passkeySupportedfromuseCavos().
React Native
import { NativePasskeyPrf } from "@cavos/kit/react-native";
const nativePrf = new NativePasskeyPrf({ rpId: "auth.example.com" });If Android does not return PRF, restore with the enclave (if enabled) rather than changing the account model.
Related
- Hardware-isolated recovery — Google/Apple unwrap of the same MasterDEK, and it can cover several chains at once.
- Vault — where passkeys and keys live on the web.
- Multi-device — opening a wallet on a new device.