> ## Documentation Index
> Fetch the complete documentation index at: https://docs.chipipay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# When Passkeys Fail

> Real failure modes — lost device, browser switch, broken biometrics — and what your app should show the user. Plus when to add a PIN backup.

Passkeys cover the happy path beautifully. The failure modes — the user buys a new laptop, switches from Chrome to Firefox, drops their phone — need explicit handling. This page is the playbook.

## The four failure modes

| What happened                                             | What the SDK throws                                                                                            | What the user sees         | What you do                                                                                                                                        |
| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| User cancelled the prompt                                 | `signShhhMessage` rejects with `NotAllowedError` translated to `Error("Passkey authentication was cancelled")` | "Authentication cancelled" | Show the prompt button again. No data loss.                                                                                                        |
| New device, no passkey synced                             | `hasShhhPasskey()` returns `false`                                                                             | Sign-up page               | If the user already has a wallet, route them to recovery (see below).                                                                              |
| PRF unavailable after a browser switch (legacy CHIPI v29) | `getWalletEncryptKey` throws `"Passkey PRF unavailable…"`                                                      | "Use PIN to sign in"       | Show PIN field. The PRF-encrypted wallet can be re-encrypted later via [useUpdateWalletEncryption](/sdk/react/hooks/use-update-wallet-encryption). |
| Lost device, no other passkey                             | nothing throws — user is locked out                                                                            | "Recover your wallet" UI   | SHHH: use the guardian recovery flow. CHIPI v29: only recoverable via the PIN backup.                                                              |

## SHHH wallets — true recovery

A SHHH wallet's owner set lives on chain. Recovery is a real on-chain primitive, not a workaround.

**Two paths:**

1. **The user still has access** (e.g. moved from old phone to new) — `propose_add_owner` adds the new passkey as a second owner with a 48-hour timelock, then `execute_add_owner` activates it. See the recovery story in [`/services/gasless/recovery`](/services/gasless/recovery).
2. **The user lost all access** — a pre-registered guardian (a second device, a recovery email's WebAuthn credential, a "Guardian-as-a-Service" partner) calls `initiate_recovery`. After a 7-day timelock the guardian can finalize a wallet rotation onto a fresh passkey.

The React side ships [`<Recover />`](/services/gasless/recovery) which drives both flows. The recipe at a glance:

```tsx theme={null}
import { Recover, useGuardianRecovery } from "@chipi-stack/chipi-react";
import { createShhhPasskey } from "@chipi-stack/chipi-passkey";

// 1. User on the new device registers a fresh passkey.
const { credentialId, publicKeyHex } = await createShhhPasskey(userId, userName);

// 2. Drop into <Recover mode="add-device" /> — see the gasless recovery docs for full wiring.
```

## CHIPI v29 wallets — dual-key fallback

A CHIPI v29 wallet's passkey only encrypts a STARK key; if the passkey is gone, the encryption key is gone with it. The mitigation is the **dual-key architecture**: store the same private key TWICE, once encrypted by the passkey, once encrypted by a user-chosen PIN.

The wallet shape carries two ciphertexts:

```ts theme={null}
type Wallet = {
  publicKey: string;
  encryptedPrivateKey: string;       // passkey-derived key encrypts this
  encryptedPrivateKeyBackup: string; // PIN encrypts this
  authMethod: "passkey+pin";         // marker for the dual-key path
};
```

Pass both at creation time and at every wallet read:

```ts theme={null}
import { useCreateWallet } from "@chipi-stack/chipi-react";
import { createWalletPasskey } from "@chipi-stack/chipi-passkey";

const { encryptKey: passkeyKey } = await createWalletPasskey(userId, userName);
const pin = await promptForPin();

await createWalletAsync({
  params: {
    externalUserId: userId,
    chain: "STARKNET",
    walletType: "CHIPI",
    encryptKey: passkeyKey,    // primary — encrypts encryptedPrivateKey
    encryptKeyBackup: pin,      // backup — encrypts encryptedPrivateKeyBackup
    authMethod: "passkey+pin",
  },
  bearerToken,
});
```

Day-to-day signing uses the passkey. If the passkey ever fails (`verifyWalletPasskeyDetailed()` returns `reason: "prf_unavailable"` or `"no_passkey"`), prompt for the PIN and use the `encryptedPrivateKeyBackup` ciphertext instead. The SDK handles which ciphertext to pick when you pass the right `encryptKey`.

## PIN-only mode is a fallback, not a default

A PIN-only wallet (no passkey) is supported, but it's the weakest path — see the PIN warning we use across every onboarding doc. Default to passkey; show PIN only when `isWebAuthnSupported()` returns `false` or the user explicitly opts in.

```tsx theme={null}
import { isWebAuthnSupported } from "@chipi-stack/chipi-passkey";

function SignUpFlow() {
  if (isWebAuthnSupported()) {
    return <PasskeySignUp />;
  }
  // Old browser, very old Android, or a corporate-managed device that
  // disables WebAuthn. Fall back to PIN.
  return <PinSignUp />;
}
```

## Browser switching (the common case)

Even fully-supported browsers don't always share PRF state. The user registers on Chrome, then opens your app in Firefox a week later. The Firefox WebAuthn ceremony succeeds — the credential is discoverable — but the PRF output is unavailable in that browser.

For a SHHH wallet, this is **not a problem**: the passkey itself is the signer, no encryption key derivation. `signShhhMessage` works on any browser that supports WebAuthn.

For a legacy CHIPI v29 wallet, this **is** the problem `verifyWalletPasskeyDetailed()` catches. Surface the PIN fallback UI, decrypt with the PIN, and offer to re-register the passkey on the new browser via the [migration hook](/sdk/react/hooks/use-migrate-wallet-to-passkey).

## Related

* [Overview](/sdk/passkeys/overview)
* [Quickstart](/sdk/passkeys/quickstart)
* [API reference](/sdk/passkeys/api)
* [Recovery on SHHH wallets](/services/gasless/recovery)
