Skip to main content

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

TypeDescriptionKEK DerivationPlatform Support
passkey_prf_hkdf_sha256WebAuthn passkey with PRF extensionHKDF(PRF_output, salt, info)iOS 18+, Android 14+
guardian_gated_escrow_no_gateAssisted Recovery (NoGate) escrowRandom KEK escrowed to backend or portable vaultAll
pbkdf2_sha256PIN or PasswordPBKDF2(credential, salt, iterations)All
noneNo encryption (dev only)N/A - plaintext sharesAll

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

FileDescription
storage/backup/BackupBundleV3.ktV3 bundle data models (internal)
storage/backup/BackupOptions.ktFlutter-facing configuration
storage/backup/UnlockStrategy.ktInternal KEK derivation strategies
storage/backup/KeyUnlockService.ktKEK derivation service
storage/backup/WalletBackupService.ktHigh-level backup operations
storage/backup/BackupEncryption.ktEncryption/decryption utilities
crypto/Hkdf.ktHKDF-SHA256 key derivation (expect/actual)
crypto/AesGcm.ktAES-256-GCM encryption (expect/actual)
crypto/Sha256.ktSHA-256 hashing (expect/actual)
crypto/KeyDerivation.ktPBKDF2 + SecureRandom (expect/actual)
passkey/PasskeyManager.ktOrchestrates passkey ceremonies
passkey/PasskeyClient.ktPlatform WebAuthn bridge (expect/actual)
passkey/WebAuthnCeremonyApi.ktBackend ceremony HTTP client
passkey/WebAuthnTypes.ktShared 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

StrategyKDFSalt RequiredCredential ReferenceSecurity Level
PasskeyPrfHKDF-SHA256YescredentialIdHighest
PinPBKDF2-SHA256 (100k iterations)YesNoneMedium
PasswordPBKDF2-SHA256 (100k iterations)YesNoneMedium-High
NoneN/ANoN/A (returns null)None (dev only)
GuardianGatedEscrow.NoGateN/AN/ARequires backend or portable vaultMedium

Capabilities Matrix

Recovery Method Comparison

CapabilityPasskey PRFPINPasswordAssisted Recovery
Offline Recovery
No Server Trust
Biometric Protection
Cross-Device Sync✅ (iCloud/Google)
Phishing Resistant⚠️
Works Offline
Platform RequirementsiOS 18+ / Android 14+AllAllAll
User MemorizationNone6 digitsComplex stringGate access

Bundle V3 Feature Support

FeatureStatusNotes
Multi-wrapper encryptionMultiple recovery methods per backup
AES-256-GCM encryptionAEAD for shares and DEK wrapping
HKDF-SHA256 (PRF)Passkey-based KEK derivation
PBKDF2-SHA256PIN/Password KEK derivation
JCS canonicalizationRFC 8785 for deterministic JSON
SHA-256 checksumIntegrity verification
Anti-rollback protectionMonotonic sequence + server pinning
Multi-algorithm supportVia 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 = null
  • dek_wraps = []
  • Shares stored as plaintext_share instead of encrypted

Security Properties

PropertyMechanismDiagram
Share confidentialityAES-256-GCM with DEKShares never stored in plaintext
KEK isolationEach wrapper has independent KEKCompromising one doesn't affect others
PRF output protectionNever exposed outside KeyUnlockServiceCleared after KEK derivation
Anti-rollbackMonotonic sequence + server pinningPrevents replay of old backups
IntegrityAEAD authentication + SHA-256 checksumTamper 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)

  • BackupBundleV3 core models
  • DekWrap and KdfParams models
  • EncryptedWallet and ShareEntry models
  • BackupOptions (Flutter-facing)
  • UnlockStrategy (internal)
  • ✅ Serialization tests

Phase 2: Key Unlock Service (Complete)

  • KeyUnlockService for 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.algorithms uses AlgorithmId enum
  • ✅ Unit tests + instrumented tests

Phase 4: Passkey Extraction (Complete)

  • PasskeyClient (expect/actual) - Platform WebAuthn bridge
    • createCredential() for registration
    • getAssertion() for authentication
    • getAssertionWithPrf() for PRF key derivation
  • WebAuthnCeremonyApi - Backend ceremony HTTP client
    • beginRegistration() / finishRegistration()
    • beginAuthentication() / finishAuthentication()
  • WebAuthnTypes - Shared data classes for WebAuthn ceremonies
  • FakePasskeyClient - Test fixture for unit testing
  • PasskeyManagerLogicTest - 13 unit tests for orchestration logic
  • PasskeyManager refactored to orchestrate PasskeyClient and WebAuthnCeremonyApi

Phase 5: Assisted Recovery (NoGate) (Complete)

  • WrapperConfig.GuardianGatedEscrow.NoGate and UnlockStrategy.GuardianGatedEscrow.NoGate
  • GuardianRecoveryModels.kt - Escrow/retrieve request + response models
  • GuardianRecoveryApiOperations - escrow + retrieve
  • RecoveryApiResult - Success and Error
  • PortableEscrowKekVault - in-memory vault (HSM simulation)
  • PortableVaultApiAdapter - adapter to GuardianRecoveryApiOperations
  • 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)

  • CredentialRegistry interface and LocalCredentialRegistry implementation
  • PasskeyService Flutter submodule for WebAuthn ceremonies
  • CredentialService Flutter submodule for credential management
  • BackupServiceModule Flutter submodule for V3 backup operations
  • GuardianRecoveryService availability surface for assisted recovery
  • FlutterWrapperConfig and FlutterUnlockStrategy DTOs
  • ✅ KSP support for kotlin.Long type mapping to Dart int
  • ✅ Old V2 API removed from BlockchainKitSdk
  • ✅ Example app migrated to new V3 submodule API
  • ⏳ Manual testing on physical devices pending

V2 vs V3 Comparison

AspectV2V3
WrappersSingle dek_wrap objectArray dek_wraps[]
Recovery methodsOne per backupMultiple per backup
Credential rotationRe-encrypt everythingAdd/remove wraps only
Assisted recovery (NoGate)Not supportedNative support
credential_typepasskey, password, nonemulti (always)
credential_referenceIn metadataPer wrapper
Anti-rollbackOptionalRecommended

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 ArtifactsPasskey AvailablePasskey Lost + Gate OKCan'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") }
)

References