Backup & Recovery V3 Architecture
Overview
Note: The PRD "Backup & Recovery (Portable Key)" is the source of truth for semantics and terminology (including "Assisted Recovery" / "Guardian-Gated Escrow Wrap"). This architecture doc may lag and should be aligned to PRD naming when in doubt.
The Backup & Recovery system uses envelope encryption with support for multiple recovery methods (multi-wrapper). This document describes the V3 bundle format and architecture.
Production backup methods are PRF (Passkey) and Guardian-Gated Escrow (Assisted Recovery, NoGate). PIN/Password wrappers are testing-only.
Primary vs secondary:
- Passkey (PRF) = primary for local encryption/decryption.
- Guardian-gated escrow = secondary if PRF is unavailable or the passkey is lost.
If a device does not support PRF, the bundle is still encrypted using KEK_ASSISTED (escrowed). Recovery uses Assisted Recovery (NoGate) to retrieve KEK_ASSISTED and decrypt the user-held bundle. Policy gates are future work.
Architecture Overview
Key Concepts
Envelope Encryption
Key Components:
- DEK (Data Encryption Key): Encrypts the actual wallet shares - random 32 bytes, unique per backup
- KEK (Key Encryption Key): Wraps the DEK, derived from user credential (PRF, PIN, Password, etc.)
- Multi-wrapper: Same DEK can have multiple KEK wrappers for different recovery methods
Multi-Wrapper Architecture
V3 Bundle Format
{
"format_version": 3,
"metadata": {
"created_at": 1706486400000,
"updated_at": 1706486400000,
"canonicalization": "jcs_rfc8785",
"encryption": "aes_256_gcm",
"algorithms": ["ecdsa_secp256k1"],
"credential_type": "multi"
},
"anti_rollback": {
"bundle_id": "uuid-v4",
"seq": 1,
"prev_checksum": null,
"policy": "server_pin"
},
"salt": "base64_16_bytes",
"dek_wraps": [
{ "wrap_type": "passkey_prf_hkdf_sha256", ... },
{ "wrap_type": "guardian_gated_escrow_no_gate", ... }
],
"wallet": {
"wallet_id": "...",
"group_id": "...",
"shares": { ... }
},
"checksum": "sha256_hex"
}
Bundle Structure Diagram
Wrapper Types
| Type | Description | KEK Derivation | Platform Support |
|---|---|---|---|
passkey_prf_hkdf_sha256 | WebAuthn passkey with PRF extension | HKDF(PRF_output, salt, info) | iOS 18+, Android 14+ |
guardian_gated_escrow_no_gate | Assisted Recovery (NoGate) escrow | Random KEK escrowed to backend or portable vault | All |
pbkdf2_sha256 | PIN or Password | PBKDF2(credential, salt, iterations) | All |
none | No encryption (dev only) | N/A - plaintext shares | All |
Complete Backup Flow
Sequence Diagram
Backup Flow Chart
Complete Restore Flow
Sequence Diagram
Restore Flow Chart
Passkey PRF Flow Detail
PRF Key Derivation
Platform Support Matrix
Key Files
| File | Description |
|---|---|
storage/backup/BackupBundleV3.kt | V3 bundle data models (internal) |
storage/backup/BackupOptions.kt | Flutter-facing configuration |
storage/backup/UnlockStrategy.kt | Internal KEK derivation strategies |
storage/backup/KeyUnlockService.kt | KEK derivation service |
storage/backup/WalletBackupService.kt | High-level backup operations |
storage/backup/BackupEncryption.kt | Encryption/decryption utilities |
crypto/Hkdf.kt | HKDF-SHA256 key derivation (expect/actual) |
crypto/AesGcm.kt | AES-256-GCM encryption (expect/actual) |
crypto/Sha256.kt | SHA-256 hashing (expect/actual) |
crypto/KeyDerivation.kt | PBKDF2 + SecureRandom (expect/actual) |
passkey/PasskeyManager.kt | Orchestrates passkey ceremonies |
passkey/PasskeyClient.kt | Platform WebAuthn bridge (expect/actual) |
passkey/WebAuthnCeremonyApi.kt | Backend ceremony HTTP client |
passkey/WebAuthnTypes.kt | Shared WebAuthn data classes |
KeyUnlockService
The KeyUnlockService is responsible for deriving KEKs from credentials:
val service = KeyUnlockService()
// PIN-based unlock
val kek = service.deriveKek(
strategy = UnlockStrategy.Pin("123456"),
bundleSalt = bundle.salt
)
// Password-based unlock
val kek = service.deriveKek(
strategy = UnlockStrategy.Password("my-secure-password"),
bundleSalt = bundle.salt
)
// Passkey PRF unlock
val kek = service.deriveKek(
strategy = UnlockStrategy.PasskeyPrf(
credentialId = "cred-id",
prfInput = prfOutput, // 32 bytes from WebAuthn PRF
),
bundleSalt = bundle.salt
)
// Clean up after use
kek?.clear()
Supported Strategies
| Strategy | KDF | Salt Required | Credential Reference | Security Level |
|---|---|---|---|---|
PasskeyPrf | HKDF-SHA256 | Yes | credentialId | Highest |
Pin | PBKDF2-SHA256 (100k iterations) | Yes | None | Medium |
Password | PBKDF2-SHA256 (100k iterations) | Yes | None | Medium-High |
None | N/A | No | N/A (returns null) | None (dev only) |
GuardianGatedEscrow.NoGate | N/A | N/A | Requires backend or portable vault | Medium |
Capabilities Matrix
Recovery Method Comparison
| Capability | Passkey PRF | PIN | Password | Assisted Recovery |
|---|---|---|---|---|
| Offline Recovery | ✅ | ✅ | ✅ | ❌ |
| No Server Trust | ✅ | ✅ | ✅ | ❌ |
| Biometric Protection | ✅ | ❌ | ❌ | ❌ |
| Cross-Device Sync | ✅ (iCloud/Google) | ❌ | ❌ | ✅ |
| Phishing Resistant | ✅ | ❌ | ❌ | ⚠️ |
| Works Offline | ✅ | ✅ | ✅ | ❌ |
| Platform Requirements | iOS 18+ / Android 14+ | All | All | All |
| User Memorization | None | 6 digits | Complex string | Gate access |
Bundle V3 Feature Support
| Feature | Status | Notes |
|---|---|---|
| Multi-wrapper encryption | ✅ | Multiple recovery methods per backup |
| AES-256-GCM encryption | ✅ | AEAD for shares and DEK wrapping |
| HKDF-SHA256 (PRF) | ✅ | Passkey-based KEK derivation |
| PBKDF2-SHA256 | ✅ | PIN/Password KEK derivation |
| JCS canonicalization | ✅ | RFC 8785 for deterministic JSON |
| SHA-256 checksum | ✅ | Integrity verification |
| Anti-rollback protection | ✅ | Monotonic sequence + server pinning |
| Multi-algorithm support | ✅ | Via algorithms[] in metadata |
| Assisted recovery (NoGate) | ✅ | KEK escrow + retrieval implemented via backend or portable vault |
Dev Mode (None Credential)
For development/testing only:
credential_type = "none"salt = nulldek_wraps = []- Shares stored as
plaintext_shareinstead of encrypted
Security Properties
| Property | Mechanism | Diagram |
|---|---|---|
| Share confidentiality | AES-256-GCM with DEK | Shares never stored in plaintext |
| KEK isolation | Each wrapper has independent KEK | Compromising one doesn't affect others |
| PRF output protection | Never exposed outside KeyUnlockService | Cleared after KEK derivation |
| Anti-rollback | Monotonic sequence + server pinning | Prevents replay of old backups |
| Integrity | AEAD authentication + SHA-256 checksum | Tamper detection |
Security Architecture
Error Handling
Implementation Status
Overall Status: ✅ All Phases Complete | ⏳ Manual Testing Pending
Note: All implementation phases (1-6) are complete. Manual device testing is required before production release. See Phase 6 changelog for test plan.
Phase 1: Data Models (Complete)
- ✅
BackupBundleV3core models - ✅
DekWrapandKdfParamsmodels - ✅
EncryptedWalletandShareEntrymodels - ✅
BackupOptions(Flutter-facing) - ✅
UnlockStrategy(internal) - ✅ Serialization tests
Phase 2: Key Unlock Service (Complete)
- ✅
KeyUnlockServicefor KEK derivation - ✅ PRF-based unlock (HKDF-SHA256)
- ✅ PBKDF2-based unlock (PIN/Password)
- ✅ None strategy (dev mode)
- ✅ GuardianGatedEscrow stub (backend required)
- ✅ 13 unit tests
Phase 3: Backup Service (Complete)
- ✅
BackupEncryption.encrypt()for None/PIN/Password - ✅
BackupEncryption.decrypt()for None/PIN/Password - ✅
WalletBackupService.addWallet()with V3 format - ✅
WalletBackupService.restoreFromBackup()with V3 format - ✅ SHA-256 checksum computation (expect/actual)
- ✅
BundleMetadata.algorithmsusesAlgorithmIdenum - ✅ Unit tests + instrumented tests
Phase 4: Passkey Extraction (Complete)
- ✅
PasskeyClient(expect/actual) - Platform WebAuthn bridgecreateCredential()for registrationgetAssertion()for authenticationgetAssertionWithPrf()for PRF key derivation
- ✅
WebAuthnCeremonyApi- Backend ceremony HTTP clientbeginRegistration()/finishRegistration()beginAuthentication()/finishAuthentication()
- ✅
WebAuthnTypes- Shared data classes for WebAuthn ceremonies - ✅
FakePasskeyClient- Test fixture for unit testing - ✅
PasskeyManagerLogicTest- 13 unit tests for orchestration logic - ✅
PasskeyManagerrefactored to orchestratePasskeyClientandWebAuthnCeremonyApi
Phase 5: Assisted Recovery (NoGate) (Complete)
- ✅
WrapperConfig.GuardianGatedEscrow.NoGateandUnlockStrategy.GuardianGatedEscrow.NoGate - ✅
GuardianRecoveryModels.kt- Escrow/retrieve request + response models - ✅
GuardianRecoveryApiOperations- escrow + retrieve - ✅
RecoveryApiResult-SuccessandError - ✅
PortableEscrowKekVault- in-memory vault (HSM simulation) - ✅
PortableVaultApiAdapter- adapter toGuardianRecoveryApiOperations - ✅
KeyUnlockService- NoGate retrieval path - ✅
BackupEncryption- NoGate escrow wrap creation - ✅ Tests: vault unit tests, iOS flow, Android instrumented flow Note: Policy gate flow is future work and not implemented in the SDK yet.
Phase 5.5: Passkey PRF Wrapper & Crypto Cleanup (Complete)
- ✅
crypto/package with organized primitives:Hkdf.kt- HKDF-SHA256 (common + android + ios)AesGcm.kt- AES-256-GCM (common + android + ios)Sha256.kt- SHA-256 (common + android + ios)KeyDerivation.kt- PBKDF2 + SecureRandom (common + android + ios)
- ✅
BackupEncryption.createPasskeyPrfWrap()- Passkey PRF wrapper creation - ✅
UnlockStrategy.PasskeyPrf- Passkey PRF unlock in decrypt() - ✅
FakePasskeyClient- Test fixture (commonTest + androidInstrumentedTest) - ✅ 7 Android instrumented tests for passkey PRF wrapper
- ✅ All 57 instrumented tests pass, all 267 unit test tasks pass
Phase 6: Flutter Integration (Complete)
- ✅
CredentialRegistryinterface andLocalCredentialRegistryimplementation - ✅
PasskeyServiceFlutter submodule for WebAuthn ceremonies - ✅
CredentialServiceFlutter submodule for credential management - ✅
BackupServiceModuleFlutter submodule for V3 backup operations - ✅
GuardianRecoveryServiceavailability surface for assisted recovery - ✅
FlutterWrapperConfigandFlutterUnlockStrategyDTOs - ✅ KSP support for
kotlin.Longtype mapping to Dartint - ✅ Old V2 API removed from
BlockchainKitSdk - ✅ Example app migrated to new V3 submodule API
- ⏳ Manual testing on physical devices pending
V2 vs V3 Comparison
| Aspect | V2 | V3 |
|---|---|---|
| Wrappers | Single dek_wrap object | Array dek_wraps[] |
| Recovery methods | One per backup | Multiple per backup |
| Credential rotation | Re-encrypt everything | Add/remove wraps only |
| Assisted recovery (NoGate) | Not supported | Native support |
| credential_type | passkey, password, none | multi (always) |
| credential_reference | In metadata | Per wrapper |
| Anti-rollback | Optional | Recommended |
Assisted Recovery (Guardian-Gated Escrow Wrap) Flow (NoGate)
NoGate has no authentication gate. The KEK is escrowed to a backend or the portable vault and can be retrieved directly by recoveryId + kekId.
Note: Policy gate variants (OTP, time locks, guardian approvals) are future work and not implemented in the SDK yet.
Backup Phase (KEK Escrow)
Recovery Phase (KEK Retrieval)
Assisted Recovery (NoGate) API
Recovery API Operations
interface GuardianRecoveryApiOperations {
suspend fun escrowKek(request: EscrowKekRequest): RecoveryApiResult<EscrowKekResponse>
suspend fun retrieveKek(request: RetrieveKekRequest): RecoveryApiResult<RetrieveKekResponse>
}
Recovery API Result Types
sealed class RecoveryApiResult<out T> {
data class Success<T>(val data: T) : RecoveryApiResult<T>()
data class Error(val message: String, val code: String? = null) : RecoveryApiResult<Nothing>()
}
DekWrap for Assisted Recovery (NoGate)
{
"wrap_id": "uuid-v4",
"wrap_type": "guardian_gated_escrow_no_gate",
"credential_reference": null,
"kdf_params": null,
"recovery_reference": {
"provider": "cramium",
"recovery_id": "recov-abc123",
"kek_id": "kek-xyz789"
},
"nonce": "base64_12_bytes",
"ciphertext_dek_escrow": "base64_wrapped_dek"
}
Disaster Recovery (2-of-3 Setup)
When Portable Key is Lost
Recovery Scenarios Matrix
Note: The Portable Recovery Kit always lives in user-controlled storage. If the user loses the bundle, recovery is irrecoverable.
| Backup Artifacts | Passkey Available | Passkey Lost + Gate OK | Can't Complete Gate |
|---|---|---|---|
| PRF + Assisted Recovery (NoGate) | ✅ Use PRF to decrypt | ✅ Assisted recovery (NoGate) → retrieve KEK | ❌ Irrecoverable |
| Assisted Recovery (NoGate) Only | ✅ Retrieve KEK (NoGate) | ✅ Assisted recovery (NoGate) → retrieve KEK | ❌ Irrecoverable |
| PRF Only | ✅ Use PRF to decrypt | ❌ Irrecoverable | ❌ Irrecoverable |
Complete Algorithm Addition Flow (V3)
This diagram shows the complete flow when adding a new algorithm to an existing wallet, including all verification and multi-wrapper handling.
Backup Bundle Creation (Full Sequence)
Usage Examples
Creating a Backup with Multiple Recovery Methods
// Configure backup with Passkey PRF + PIN fallback
val options = BackupOptions(
wrappers = listOf(
WrapperConfig.PasskeyPrf,
WrapperConfig.Pin("123456")
)
)
// Create backup
val result = walletBackupService.addWallet(
groupId = walletGroup.groupId,
options = options
)
result.fold(
onSuccess = { bundleId -> println("Backup created: $bundleId") },
onFailure = { error -> println("Backup failed: $error") }
)
Restoring with Passkey PRF
// User authenticates with passkey - PRF output derived automatically
val prfResult = passkeyManager.authenticateWithPrf(bundle.salt)
when (prfResult) {
is PasskeyResult.Success -> {
val strategy = UnlockStrategy.PasskeyPrf(
credentialId = prfResult.data.credentialId,
prfInput = prfResult.data.prfSecret
)
val restoreResult = walletBackupService.restoreFromBackup(strategy)
// Handle result...
}
is PasskeyResult.Cancelled -> println("User cancelled")
is PasskeyResult.Error -> println("Error: ${prfResult.exception}")
}
Restoring with PIN
val strategy = UnlockStrategy.Pin("123456")
val result = walletBackupService.restoreFromBackup(strategy)
result.fold(
onSuccess = { walletGroup -> println("Restored: ${walletGroup.groupId}") },
onFailure = { error -> println("Restore failed: $error") }
)