> ## 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 password-protected keys

> How to register and use PasswordProtectedKey credentials, where DFNS stores an encrypted signing key that only the user can decrypt with their password.

A `PasswordProtectedKey` credential is a signing key whose private key is encrypted in your frontend and stored by DFNS as an opaque blob. DFNS never sees the password. During login and action signing, DFNS returns the encrypted key so the user can decrypt it locally with their password, sign, and discard the plaintext.

<Note>
  Password-protected keys let a user authenticate and sign with a password instead of a passkey. Use them when WebAuthn is unavailable or undesirable — for example, non-browser environments, or a credential the user needs across domains (passkeys are bound to a single domain). You own the full experience: you decide the encryption scheme, the password format, and the UX. The password never leaves the client.
</Note>

## How password-protected keys work

A `PasswordProtectedKey` carries an opaque encrypted blob that DFNS stores and returns to you. You implement the encryption; the user keeps the password.

* On **registration**, you send the public key plus the encrypted private key as `encryptedPrivateKey`.
* On **login** and **action signing**, DFNS returns the same blob back to you — as `encryptedPrivateKey` on each entry of `allowCredentials.passwordProtectedKey`. The user decrypts it with their password, signs the challenge, and never exposes the plaintext key.

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

Unlike a [`RecoveryKey`](/guides/developers/end-user-recovery), using a password-protected key does **not** invalidate the user's other credentials — it is a normal first-factor signing credential.

## Implementing password-protected keys

<Note>
  If implementing this 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 keypair and encrypt it with the user's password">
    When the user registers, generate a keypair and encrypt the private key with their password. This must happen on the client side — the password must never reach your server.

    ```typescript title="Frontend - Password-protected key generation" theme={null}
    import crypto from 'crypto'

    // Generate a signing 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' })

    // The user's password — collected in your UI, never sent to your server
    const password = await promptUser('Choose a password to protect your key')

    // Derive an encryption key from the password
    const salt = crypto.randomBytes(16)
    const encryptionKey = crypto.pbkdf2Sync(password, 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,
    })
    ```

    <Note>
      This AES-256-GCM + PBKDF2 scheme is one example. You choose the scheme — DFNS treats `encryptedPrivateKey` as an opaque string.
    </Note>
  </Step>

  <Step title="Register the password-protected key with DFNS">
    Send the public key and `encryptedPrivateKey` in the [Complete User Registration](/api-reference/auth/complete-user-registration) call (or [Complete End User Registration with Wallets](/api-reference/auth/complete-end-user-registration-with-wallets)). The password stays with the user.

    **Which challenge to use.** The credential signs the same challenge returned by the registration init endpoint ([Create Registration Challenge](/api-reference/auth/create-registration-challenge), [Create Delegated Registration Challenge](/api-reference/auth/create-delegated-registration-challenge), or [Create Social Registration Challenge](/api-reference/auth/create-social-registration-challenge)). All credentials in the same registration call share that one challenge.

    Build the `clientData` manually, since this is a key-style credential and not a Fido2 passkey. For registration, `type` is `key.create`:

    ```typescript title="Frontend - Build clientData for the PasswordProtectedKey" theme={null}
    // Keys must be alphabetically sorted, no spaces in JSON separators
    const clientData = {
      challenge: registrationChallenge, // from the registration init call
      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 passwordProtectedKeyCredential = {
      credentialKind: 'PasswordProtectedKey',
      credentialInfo: {
        credId: generateCredentialId(),
        clientData: clientDataBase64,       // built above
        attestationData: attestationBase64, // contains the public key
      },
      encryptedPrivateKey, // encrypted blob only, password stays with user
    }
    ```

    To add a password-protected key **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="Log the user in with the password-protected key">
    Login is a three-call flow, because [Create Login Challenge](/api-reference/auth/create-login-challenge) is unauthenticated and returns the encrypted key. To stop anyone from fetching a user's encrypted key to brute-force offline, DFNS first requires a one-time login code that proves the user controls their account.

    ```typescript title="1. Send a one-time login code" theme={null}
    // The client is unauthenticated at this point — no token yet.
    // DFNS emails a one-time code to the user.
    await dfnsClient.auth.sendLoginCode({ body: { username, orgId } })
    const loginCode = await promptUser('Enter the login code we emailed you')
    ```

    ```typescript title="2. Create the login challenge with the code" theme={null}
    const challenge = await dfnsClient.auth.createLoginChallenge({
      body: { username, orgId, loginCode },
    })

    // The encrypted key is returned here, keyed by credential.
    const { encryptedPrivateKey } = challenge.allowCredentials.passwordProtectedKey[0]
    ```

    ```typescript title="3. Decrypt, sign, and complete login" theme={null}
    // Prompt for the password and decrypt the private key
    const password = await promptUser('Enter your password')
    const encryptedData = JSON.parse(encryptedPrivateKey)

    const encryptionKey = crypto.pbkdf2Sync(
      password,
      Buffer.from(encryptedData.salt, 'base64'),
      100000,
      32,
      'sha256'
    )

    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')

    // Build the clientData — type MUST be 'key.get' for login, keys sorted, no spaces
    const clientData = JSON.stringify({
      challenge: challenge.challenge, // already base64url — do not re-encode
      type: 'key.get',
    })

    // Sign the clientData BYTES with the decrypted private key
    const signingKey = crypto.createPrivateKey(privateKeyPem)
    const signature = crypto.sign(undefined, Buffer.from(clientData), signingKey)

    // Complete login
    const { token } = await dfnsClient.auth.login({
      body: {
        challengeIdentifier: challenge.challengeIdentifier,
        firstFactor: {
          kind: 'PasswordProtectedKey',
          credentialAssertion: {
            credId: challenge.allowCredentials.passwordProtectedKey[0].id,
            clientData: Buffer.from(clientData).toString('base64url'),
            signature: signature.toString('base64url'), // base64url, not standard base64
          },
        },
      },
    })
    ```

    <Warning>
      Sign the **`clientData` bytes**, not the raw challenge. Encode both `clientData` and `signature` as **base64url** — standard base64 causes a 400 error.
    </Warning>
  </Step>

  <Step title="Sign user actions">
    Non-read-only API calls require a [user action signature](/api-reference/auth/signing-flows). The flow mirrors login, but no login code is needed — the user is already authenticated, so [Create User Action Challenge](/api-reference/auth/create-user-action-challenge) returns the encrypted key directly.

    ```typescript title="Sign a user action with a password-protected key" theme={null}
    const challenge = await dfnsClient.auth.createUserActionChallenge({
      body: { userActionPayload, userActionHttpMethod, userActionHttpPath },
    })

    // Decrypt the key and build the 'key.get' clientData exactly as in login,
    // then submit the assertion:
    const { userAction } = await dfnsClient.auth.createUserActionSignature({
      body: {
        challengeIdentifier: challenge.challengeIdentifier,
        firstFactor: {
          kind: 'PasswordProtectedKey',
          credentialAssertion: {
            credId: challenge.allowCredentials.passwordProtectedKey[0].id,
            clientData: clientDataBase64url,
            signature: signatureBase64url,
          },
        },
      },
    })
    // Pass userAction as the X-DFNS-USERACTION header on the original request.
    ```
  </Step>
</Steps>

## Security considerations

* **Password strength** — Encourage strong passwords, and rate-limit login and signing to slow brute-force attempts against the encrypted blob.
* **Keep decryption on the client** — Never send the password or the decrypted key to your server. Consider a [Service Worker](/api-reference/auth/credentials#security-of-key-based-credentials) to perform crypto in a secure context.
* **Register a recovery path** — A user who forgets their password loses that credential. Pair password-protected keys with a second credential or a [recovery credential](/guides/developers/end-user-recovery).

## Related

<CardGroup>
  <Card title="Credentials" href="/api-reference/auth/credentials">
    Overview of credential kinds, including password-protected keys.
  </Card>

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

  <Card title="Authentication flows" href="/api-reference/auth/login-flows">
    The full login flow, including the login-code step.
  </Card>

  <Card title="Registration flows" href="/api-reference/auth/registration-flows">
    How credentials are registered.
  </Card>
</CardGroup>
