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

# Implement end-user recovery

> How to implement recovery flows for delegated wallet users so they can regain access if they lose their device or passkey credential.

When using [delegated wallets](/guides/developers/delegated-wallets), your end users control their own signing credentials. If they lose access to their device, they need a way to recover their wallet. This guide covers how to implement recovery flows.

<Note>
  Recovery credentials are encrypted private keys that DFNS stores as an opaque blob. The encryption happens in your frontend. DFNS never sees the decryption password. This means you own the full recovery experience: you decide the encryption scheme, the password format, and the recovery UX. The DFNS dashboard recovery flow at [app.dfns.io/recover](https://app.dfns.io/recover) only works for users who registered their recovery credential through the dashboard.
</Note>

## Recovery strategies

| Strategy                  | How it works                                   | Best for                    |
| ------------------------- | ---------------------------------------------- | --------------------------- |
| **Secondary credentials** | User registers credentials on multiple devices | Users with multiple devices |
| **Recovery credential**   | User stores a recovery password securely       | Self-service recovery       |

We recommend encouraging users to register credentials on multiple devices as the primary recovery method. Recovery credentials provide a fallback when that's not possible. Users can register multiple recovery credentials. One is typically created during initial registration, and more can be added later via the [Create Credential](/api-reference/auth/create-credential) flow.

<Tip>
  **Users should always have at least one recovery credential available.** Generate a recovery credential for your users during registration, and when a recovery credential is used (which invalidates all existing credentials), generate a new one as part of the recovery flow. This way users are never left without a recovery path.
</Tip>

<Tip>
  **Nudge users to register a second credential.** Consider prompting users to add a backup credential right after their initial registration, and showing a persistent security banner in your app until they do. End users who lose their only credential and have no recovery key will need you to initiate a [delegated recovery](/api-reference/auth/create-delegated-recovery-challenge) on their behalf. Proactive nudges reduce that support burden.
</Tip>

<Warning>
  You could store recovery credentials server-side and release them after identity verification (KYC, etc.), but this is not recommended. Whoever controls the decryption password can take control of the wallet, which undermines the delegated signing model.
</Warning>

## How recovery credentials work

A `RecoveryKey` credential uses an `encryptedPrivateKey` field - an opaque string that DFNS stores and returns to you. You implement the encryption, and the user keeps the decryption password.

<Note>
  DFNS stores the encrypted blob but never has access to the decryption password. Only the user can decrypt and use the recovery key.
</Note>

When used, a recovery credential triggers the recovery flow which **invalidates all existing credentials** for security.

## Implementing user-held recovery

<Note>
  If implementing recovery in a browser without Node.js, use the [@dfns/sdk-browser](/sdks/frontend/typescript/development#browserkeysigner) package for signing operations. If you must implement manually, see [Base64Url encoding](/guides/developers/generate-a-key-pair#base64-and-base64url-encoding) for correct encoding functions.
</Note>

<Steps>
  <Step title="Generate a recovery keypair during registration">
    When a user registers, generate a recovery keypair and encrypt the private key with a password. This must happen on the client side - the password should never be sent to your server.

    ```typescript title="Frontend - Recovery credential generation" theme={null}
    import crypto from 'crypto'

    // Generate a recovery keypair
    const { publicKey, privateKey } = crypto.generateKeyPairSync('ec', {
      namedCurve: 'prime256v1',
    })

    // Export keys
    const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' })
    const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' })

    // Generate a random recovery password for the user
    const recoveryPassword = crypto.randomBytes(16).toString('base64') // Or use a word-based format

    // Derive an encryption key from the password
    const salt = crypto.randomBytes(16)
    const encryptionKey = crypto.pbkdf2Sync(recoveryPassword, salt, 100000, 32, 'sha256')

    // Encrypt the private key
    const iv = crypto.randomBytes(16)
    const cipher = crypto.createCipheriv('aes-256-gcm', encryptionKey, iv)

    let encrypted = cipher.update(privateKeyPem, 'utf8', 'base64')
    encrypted += cipher.final('base64')
    const authTag = cipher.getAuthTag()

    const encryptedPrivateKey = JSON.stringify({
      salt: salt.toString('base64'),
      iv: iv.toString('base64'),
      authTag: authTag.toString('base64'),
      data: encrypted,
    })
    ```
  </Step>

  <Step title="Display the recovery password to the user">
    Show the recovery password to the user with clear instructions:

    ```
    Your recovery password: Kx7mP2nQ9vBw3rYt

    Store this securely - you'll need it to recover your wallet if you lose access to your device.
    - Save it in a password manager
    - Write it down and store in a safe place
    - Do not share it with anyone
    ```

    The user must store this password themselves. You should not store it.
  </Step>

  <Step title="Register the recovery credential with DFNS">
    Bundle the recovery credential into the same `Complete User Registration` (or `Complete End User Registration with Wallets`) call as the first passkey. The public key and `encryptedPrivateKey` are sent to DFNS. The password stays with the user.

    **Which challenge to use.** The `RecoveryKey` credential signs the same challenge returned by the registration init endpoint (`Create Delegated Registration Challenge`, `Create Registration Challenge`, or `Create Social Registration Challenge`). All credentials in the same registration call share that one challenge.

    Build the `clientData` for the recovery credential manually, since it is a key-style credential and not a Fido2 passkey:

    ```typescript title="Frontend - Build clientData for the RecoveryKey" theme={null}
    // Keys must be alphabetically sorted, no spaces in JSON separators
    const clientData = {
      challenge: registrationChallenge, // from Create (Delegated) Registration Challenge
      type: 'key.create',
    }
    const clientDataJson = JSON.stringify(clientData)
    const clientDataBase64 = Buffer.from(clientDataJson).toString('base64url')
    ```

    See [Credentials Data](/api-reference/auth/credentials-data#key-password-protected-key-and-recovery-credential) for the exact `clientData` and [`attestationData`](/api-reference/auth/credentials-data#key-password-protected-key-and-recovery-credential-2) formatting rules. Incorrect stringification causes `Unable to verify signature` errors.

    ```typescript title="Frontend - Include in registration request" theme={null}
    const recoveryCredential = {
      credentialKind: 'RecoveryKey',
      credentialInfo: {
        credId: generateCredentialId(),
        clientData: clientDataBase64,       // built above
        attestationData: attestationBase64, // contains the public key
      },
      encryptedPrivateKey, // encrypted blob only, password stays with user
    }
    ```

    To add a recovery credential **after** registration instead, use the [Create Credential](/api-reference/auth/credentials#regular-flow) flow. The challenge then comes from `Create Credential Challenge` (or `Create Credential Challenge With Code`); the rest of the construction is identical.
  </Step>

  <Step title="Implement the recovery flow">
    Your server calls [Create Delegated Recovery Challenge](/api-reference/auth/create-delegated-recovery-challenge) with a service-account token to initiate recovery. This returns a `temporaryAuthenticationToken`, the `challenge`, and `allowedRecoveryCredentials` (which contains the `encryptedPrivateKey`). Pass these to the client — not your service-account token. All decryption and signing then happens on the client side.

    ```typescript title="Server - Initiate recovery" theme={null}
    const { temporaryAuthenticationToken, challenge, allowedRecoveryCredentials } =
      await dfnsClient.auth.createDelegatedRecoveryChallenge({
        body: { username, credentialId: recoveryCredentialId },
      })
    // Send temporaryAuthenticationToken, challenge, and allowedRecoveryCredentials to the client.
    ```

    ```typescript title="Client - Sign and complete recovery" theme={null}
    // 1. Prompt user for their recovery password
    const recoveryPassword = await promptUser('Enter your recovery password')

    // 2. Get the encrypted recovery key from the allowedRecoveryCredentials returned above
    const encryptedData = JSON.parse(allowedRecoveryCredentials[0].encryptedRecoveryKey)

    // 3. Derive the encryption key from the user's password
    const encryptionKey = crypto.pbkdf2Sync(
      recoveryPassword,
      Buffer.from(encryptedData.salt, 'base64'),
      100000,
      32,
      'sha256'
    )

    // 4. Decrypt the private key
    const decipher = crypto.createDecipheriv(
      'aes-256-gcm',
      encryptionKey,
      Buffer.from(encryptedData.iv, 'base64')
    )
    decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'base64'))

    let privateKeyPem = decipher.update(encryptedData.data, 'base64', 'utf8')
    privateKeyPem += decipher.final('utf8')

    // 5. Build the new credentials wrapper — this is what you send in the body
    //    and what the recovery key commits to in the challenge.
    const newCredentials = {
      firstFactorCredential: newCredential,      // WebAuthn / Fido2 attestation created above
      recoveryCredential: newRecoveryCredential, // strongly recommended — rotates the recovery key
    }

    // 6. Build the clientData for the recovery assertion.
    //    type MUST be 'key.get'
    //    challenge MUST be base64url(JSON.stringify(newCredentials)) — the whole wrapper
    const clientData = JSON.stringify({
      type: 'key.get',
      challenge: Buffer.from(JSON.stringify(newCredentials)).toString('base64url'),
      origin: window.location.origin,
      crossOrigin: false,
    })

    // 7. Sign the clientData BYTES with the decrypted recovery private key.
    //    You sign clientData, NOT newCredentials directly.
    const recoveryKey = crypto.createPrivateKey(privateKeyPem)
    const signature = crypto.sign(undefined, Buffer.from(clientData), recoveryKey)

    // 8. Complete recovery — authenticate with the temporaryAuthenticationToken from the server
    const userClient = new DfnsApiClient({
      authToken: temporaryAuthenticationToken,
      baseUrl: process.env.NEXT_PUBLIC_DFNS_BASE_URL,
    })
    await userClient.auth.recoverUser({
      body: {
        recovery: {
          kind: 'RecoveryKey',
          credentialAssertion: {
            credId: recoveryCredentialId,
            clientData: Buffer.from(clientData).toString('base64url'),
            signature: signature.toString('base64url'), // base64url, not standard base64
          },
        },
        newCredentials, // same wrapper committed to in step 6
      },
    })
    ```

    <Warning>
      The recovery key signs the **`clientData` bytes**, not the credential JSON. Signing `JSON.stringify(newCredentials)` directly produces `challenge doesn't match` or `Invalid signature of Key credential assertion`.
    </Warning>

    <Note>
      Encode `signature` as **base64url** — use `.toString('base64url')` in Node.js. Standard base64 will cause a 400 error.
    </Note>
  </Step>

  <Step title="Generate new recovery credentials">
    After recovery, all previous credentials (including recovery credentials) are invalidated. Always generate a new recovery credential and display the new recovery password to the user. Do not let the user leave the recovery flow without a fresh recovery credential in place.
  </Step>
</Steps>

## Security considerations

* **Password strength** - Generate strong random passwords. Consider using word-based formats (like BIP39) for easier transcription.
* **Clear user instructions** - Users must understand the importance of storing their recovery password securely.
* **Rate limiting** - Prevent brute-force attempts on recovery flows.

## Related

<CardGroup>
  <Card title="Account Recovery" href="/api-reference/auth/account-recovery">
    Overview of recovery mechanisms for users.
  </Card>

  <Card title="Credentials Data" href="/api-reference/auth/credentials-data">
    How to build Client Data and Attestation Data objects.
  </Card>

  <Card title="Create Delegated Recovery Challenge" href="/api-reference/auth/create-delegated-recovery-challenge">
    API endpoint to initiate recovery for an end user.
  </Card>

  <Card title="Recover User" href="/api-reference/auth/recover-user">
    API endpoint to complete user recovery.
  </Card>
</CardGroup>
