When using 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.
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.
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 non-custodial model of delegated signing.
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.
Dfns stores the encrypted blob but never has access to the decryption password. Only the user can decrypt and use the recovery key.
When used, a recovery credential triggers the recovery flow which invalidates all existing credentials for security.
Implementing User-Held Recovery
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.Frontend - Recovery credential generation
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,
})
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. Register the recovery credential with Dfns
Include the recovery credential when registering the user. The public key and encryptedPrivateKey are sent to Dfns - the password stays with the user.You’ll need to build the Client Data and Attestation Data objects as described in the credentials documentation.Frontend - Include in registration request
const recoveryCredential = {
credentialKind: 'RecoveryKey',
credentialInfo: {
credId: generateCredentialId(),
clientData: clientDataBase64, // See credentials-data docs
attestationData: attestationBase64, // Contains the public key - see credentials-data docs
},
encryptedPrivateKey: encryptedPrivateKey, // Encrypted blob only, password stays with user
}
Implement the recovery flow
When a user needs to recover, all decryption happens on the client side:// 1. Prompt user for their recovery password
const recoveryPassword = await promptUser('Enter your recovery password')
// 2. Get the encrypted recovery key from Dfns (returned during recovery init)
const encryptedData = JSON.parse(encryptedPrivateKey)
// 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. Sign the recovery challenge with the decrypted key
const recoveryKey = crypto.createPrivateKey(privateKeyPem)
const newCredential = { /* user's new passkey */ }
const signature = crypto.sign(
undefined,
Buffer.from(JSON.stringify(newCredential)),
recoveryKey
)
// 6. Complete recovery - send signature to Dfns
await dfnsClient.auth.recoverUser({
body: {
recovery: {
credentialAssertion: {
credId: recoveryCredentialId,
clientData: clientDataBase64, // See credentials-data docs
signature: signature.toString('base64'),
}
},
newCredential,
}
})
Generate new recovery credentials
After recovery, all previous credentials are invalidated. Generate and display a new recovery password to the user (again, on the client side).
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.