Backup & Recovery Flutter API
Status: ✅ Implementation Complete | ⏳ Manual Testing Pending Last Updated: 2026-02-01
Overview
This document describes the Flutter API for backup, recovery, passkey, and credential management. The API is organized into four domain-specific services, each with clear responsibilities.
Note: The API implementation is complete as of Phase 6. Manual testing on physical devices is pending before production release.
Architecture
BlockchainKitSdk
├── passkeyService → WebAuthn ceremonies
├── credentialService → Credential registry management
├── backupService → Backup bundle operations
├── recoveryService → Guardian-gated recovery (coming soon)
├── mpc → BLE + MPC operations
└── blockchainService → Blockchain operations
Services
PasskeyService
Handles WebAuthn registration and authentication ceremonies.
class PasskeyService {
/// Register a new passkey via WebAuthn ceremony.
/// Internally adds credential to CredentialRegistry.
Future<Result<CredentialInfo, PasskeyError>> register({
String? displayName,
});
/// Authenticate with passkey via WebAuthn ceremony.
Future<Result<AuthResult, PasskeyError>> authenticate();
/// Check if platform supports PRF extension.
/// iOS 18+ and Android 14+ support PRF.
Future<Result<bool, PasskeyError>> isPrfSupported();
}
CredentialService
Manages the local registry of WebAuthn credentials.
class CredentialService {
/// Get all registered credentials for current account.
Future<Result<List<CredentialInfo>, CredentialError>> getCredentials();
/// Get the default credential used for authentication.
Future<Result<CredentialInfo?, CredentialError>> getDefaultCredential();
/// Get only PRF-capable credentials (for backup encryption).
Future<Result<List<CredentialInfo>, CredentialError>> getPrfCapableCredentials();
/// Set a credential as the default.
Future<Result<bool, CredentialError>> setDefaultCredential(String credentialId);
/// Revoke a credential (marks as revoked, doesn't delete).
Future<Result<bool, CredentialError>> revokeCredential(String credentialId);
/// Permanently delete a credential from registry.
Future<Result<bool, CredentialError>> deleteCredential(String credentialId);
}
BackupService
Handles V3 backup bundle operations with multi-wrapper support.
class BackupService {
// === Query ===
/// Check if a backup exists in storage.
Future<Result<bool, BackupError>> hasBackup();
/// Get backup metadata without decryption.
Future<Result<BackupInfo?, BackupError>> getBackupInfo();
// === Create / Restore ===
/// Create a V3 backup with specified wrappers.
Future<Result<BackupResult, BackupError>> createBackup({
required String groupId,
required List<WrapperConfig> wrappers,
List<StorageLocation> storageLocations = const [StorageLocation.cloud],
});
/// Restore wallet from backup using specified unlock strategy.
Future<Result<List<WalletRestoreResult>, BackupError>> restore({
required UnlockStrategy strategy,
StorageLocation? fromLocation,
});
// === Wrapper Management ===
/// Add a new wrapper to existing backup (same DEK, new KEK wrap).
Future<Result<BackupResult, BackupError>> addWrapper(WrapperConfig wrapper);
/// Remove a wrapper from backup.
Future<Result<BackupResult, BackupError>> removeWrapper(String wrapId);
// === Storage Operations ===
/// Check if export is supported by current provider.
Future<Result<bool, BackupError>> isExportSupported();
/// Export backup to device Downloads folder.
Future<Result<String, BackupError>> exportToDownloads();
/// Import backup from file path.
Future<Result<bool, BackupError>> importFromFile(String filePath);
// === System Backup ===
/// Check if system backup (iCloud/Google Backup) is enabled.
Future<Result<bool, BackupError>> isSystemBackupEnabled();
/// Open system backup settings.
Future<Result<bool, BackupError>> openSystemBackupSettings();
}
RecoveryService (Coming Soon)
Handles guardian-gated recovery flow. Backend not yet available.
class RecoveryService {
/// Start guardian-gated recovery. Sends OTP via gate contact.
Future<Result<RecoveryChallenge, RecoveryError>> initiateRecovery(String gateContact);
/// Verify OTP code. Starts time-lock on success.
Future<Result<RecoveryStatus, RecoveryError>> verifyOtp(
String challengeId,
String code,
);
/// Resend OTP via gate contact.
Future<Result<bool, RecoveryError>> resendOtp(String challengeId);
/// Get current recovery status (poll for time-lock).
Future<Result<RecoveryStatus, RecoveryError>> getStatus(String challengeId);
/// Cancel ongoing recovery.
Future<Result<bool, RecoveryError>> cancelRecovery(String challengeId);
/// Complete recovery: register new credential + retrieve KEK.
Future<Result<RecoveryCompletion, RecoveryError>> completeRecovery(
String challengeId,
NewCredentialConfig credential,
);
}
Data Models
WrapperConfig
Specifies how to wrap the DEK for a recovery method.
sealed class WrapperConfig {
/// Passkey PRF-based encryption.
/// Requires explicit credential ID - no auto-expansion for security.
factory WrapperConfig.passkeyPrf({required String credentialId});
/// Guardian-gated recovery (KEK escrowed to Guardian).
factory WrapperConfig.guardianGatedEscrow({required String gateContact});
/// PIN-based encryption (PBKDF2-SHA256).
factory WrapperConfig.pin(String pin);
/// Password-based encryption (PBKDF2-SHA256).
factory WrapperConfig.password(String password);
/// No encryption (DEV/TEST ONLY).
factory WrapperConfig.none();
}
UnlockStrategy
Specifies how to derive KEK for decryption.
sealed class UnlockStrategy {
/// Passkey PRF-based unlock.
factory UnlockStrategy.passkeyPrf({required String credentialId});
/// Guardian-gated recovery unlock.
factory UnlockStrategy.guardianGatedEscrow({
required String recoveryId,
required String kekId,
required String challengeId,
String? passkeyProof,
});
/// PIN-based unlock.
factory UnlockStrategy.pin(String pin);
/// Password-based unlock.
factory UnlockStrategy.password(String password);
/// No encryption (DEV/TEST ONLY).
factory UnlockStrategy.none();
}
StorageLocation
Where to store/retrieve backups.
enum StorageLocation {
cloud, // Auto-synced via iCloud/Google Backup
export, // Manual export to Downloads
nfcCard, // NFC backup card (coming soon)
}
CredentialInfo
Information about a registered credential.
class CredentialInfo {
final String id; // base64url credential ID
final String displayName; // "iPhone 15", "YubiKey 5"
final CredentialType type; // passkey, securityKey
final bool prfCapable; // Supports PRF extension
final int createdAt; // Unix timestamp ms
final int lastUsedAt; // Unix timestamp ms
final CredentialStatus status; // active, revoked
}
enum CredentialType { passkey, securityKey }
enum CredentialStatus { active, revoked }
BackupInfo
Metadata about a backup bundle (without decryption).
class BackupInfo {
final String bundleId;
final int formatVersion; // Should be 3
final int createdAt;
final int updatedAt;
final List<WrapperInfo> wrappers;
final List<String> algorithms; // ["ecdsa_secp256k1"]
}
class WrapperInfo {
final String wrapId;
final String type; // "passkey_prf", "guardian_gated_escrow", "pin", "password"
final String? credentialId; // For passkey_prf wrappers
final int? expiresAt; // For guardian_gated_escrow wrappers
}
Recovery Models
class RecoveryChallenge {
final String challengeId;
final int expiresAt; // OTP expiration
}
class RecoveryStatus {
final RecoveryState state;
final int? timeLockEndsAt;
final int? attemptsRemaining;
}
enum RecoveryState {
pendingOtp,
timeLockActive,
readyForCredential,
completed,
expired,
cancelled,
}
class RecoveryCompletion {
final String credentialId; // New credential registered
final bool kekRetrieved; // KEK ready for backup restore
}
sealed class NewCredentialConfig {
factory NewCredentialConfig.passkey({String? displayName});
factory NewCredentialConfig.pin(String pin);
factory NewCredentialConfig.password(String password);
}
Error Types
PasskeyError
sealed class PasskeyError {
factory PasskeyError.cancelled();
factory PasskeyError.prfNotSupported();
factory PasskeyError.ceremonyFailed(String message);
factory PasskeyError.notRegistered();
factory PasskeyError.unknown(String message);
}
CredentialError
sealed class CredentialError {
factory CredentialError.notFound(String credentialId);
factory CredentialError.alreadyRevoked(String credentialId);
factory CredentialError.cannotDeleteDefault();
factory CredentialError.unknown(String message);
}
BackupError
sealed class BackupError {
factory BackupError.notFound();
factory BackupError.decryptionFailed();
factory BackupError.corrupted();
factory BackupError.wrapperNotFound(String wrapType);
factory BackupError.exportNotSupported();
factory BackupError.storageUnavailable(StorageLocation location);
factory BackupError.unknown(String message);
}
RecoveryError
sealed class RecoveryError {
factory RecoveryError.invalidOtp(int attemptsRemaining);
factory RecoveryError.otpExpired();
factory RecoveryError.timeLockActive(int endsAt);
factory RecoveryError.challengeNotFound(String challengeId);
factory RecoveryError.challengeExpired();
factory RecoveryError.alreadyCompleted();
factory RecoveryError.serverError(String message);
factory RecoveryError.unknown(String message);
}
Usage Flows
User Tiers
Based on MPC-PROD1, there are three user tiers with different backup models:
| Tier | Guardian-Gated Recovery | KEK Storage | Backup Location |
|---|---|---|---|
| Novice | ✅ Enabled | Server (escrowed) | Cloud (auto) |
| Normal | ❌ Disabled | Local only | Cloud (auto) |
| Advanced | ❌ Disabled | Local only | Manual export |
Flow 1: First-time Novice User (Guardian-Gated Recovery)
final sdk = BlockchainKitSdk();
// 1. Register passkey
final credential = await sdk.passkeyService.register(
displayName: "iPhone 15",
);
// 2. Create 2-of-2 wallet
final wallet = await sdk.create2of2Wallet("My Wallet");
// 3. Create backup with Passkey PRF + Guardian-Gated
await sdk.backupService.createBackup(
groupId: wallet.groupId,
wrappers: [
WrapperConfig.passkeyPrf(credentialId: credential.id),
WrapperConfig.guardianGatedEscrow(), // KEK escrowed to Guardian
],
);
Flow 2: Normal User (Self-Sovereign)
// Same as above but without guardian-gated escrow
await sdk.backupService.createBackup(
groupId: wallet.groupId,
wrappers: [
WrapperConfig.passkeyPrf(credentialId: credential.id),
// No guardianGatedEscrow() - fully self-sovereign
],
);
Flow 3: Advanced User (Manual Export)
// Create backup for export
await sdk.backupService.createBackup(
groupId: wallet.groupId,
wrappers: [
WrapperConfig.passkeyPrf(credentialId: credential.id),
],
storageLocations: [StorageLocation.export],
);
// Export to downloads folder
final filePath = await sdk.backupService.exportToDownloads();
// User saves this file to their secure location
Flow 4: Restore with Passkey Available (Happy Path)
// 1. Authenticate with synced passkey
await sdk.passkeyService.authenticate();
// 2. Check backup info
final info = await sdk.backupService.getBackupInfo();
// 3. Find PRF wrapper
final prfWrapper = info.wrappers.firstWhere(
(w) => w.type == "passkey_prf",
);
// 4. Restore
final wallets = await sdk.backupService.restore(
strategy: UnlockStrategy.passkeyPrf(
credentialId: prfWrapper.credentialId!,
),
);
Flow 5: Restore with Passkey Lost (Novice - Guardian-Gated Recovery)
// 1. Initiate guardian-gated recovery
final challenge = await sdk.recoveryService.initiateRecovery(
"gate:contact:example", // gate contact identifier
);
// 2. User enters OTP from gate contact
await sdk.recoveryService.verifyOtp(
challenge.challengeId,
"123456",
);
// 3. Poll for time-lock expiry
RecoveryStatus status;
do {
await Future.delayed(Duration(seconds: 30));
status = await sdk.recoveryService.getStatus(challenge.challengeId);
} while (status.state == RecoveryState.timeLockActive);
// 4. Register new passkey + retrieve KEK
await sdk.recoveryService.completeRecovery(
challenge.challengeId,
NewCredentialConfig.passkey(displayName: "Recovery iPhone"),
);
// 5. Restore using assisted recovery
final wallets = await sdk.backupService.restore(
strategy: UnlockStrategy.assistedRecovery(
challengeId: challenge.challengeId,
),
);
Flow 6: Upgrade to 2-of-3 (Add Silicon Key)
// 1. Scan for AC device
final devices = await sdk.mpc.scanForDevices();
// 2. Connect to device
await sdk.mpc.connectToDevice(devices.first.id);
// 3. Identity exchange
await sdk.mpc.performIdentityExchange(wallet.groupId);
// 4. DKG to add Silicon Key
final dkgResult = await sdk.mpc.performDkg(wallet.groupId);
// 5. MANDATORY: Update backup after key resharing
await sdk.backupService.createBackup(
groupId: wallet.groupId,
wrappers: [
WrapperConfig.passkeyPrf(credentialId: credential.id),
WrapperConfig.guardianGatedEscrow(),
],
);
Flow 7: Transaction Signing (2-of-2 Swift Mode)
// Portable + Guardian signing
final preImage = await sdk.blockchainService.preImageHash(tx, from, fee);
final signedTx = await sdk.blockchainService.sign(preImage, groupId);
await sdk.blockchainService.sendTransaction(network, signedTx);
Flow 8: Transaction Signing (2-of-3 Sovereign Mode)
// Portable + Silicon signing (requires connected AC)
await sdk.mpc.connectToDevice(deviceId);
await sdk.mpc.performPresign(groupId);
final preImage = await sdk.blockchainService.preImageHash(tx, from, fee);
final signedTx = await sdk.blockchainService.signWithHw(preImage, groupId);
await sdk.blockchainService.sendTransaction(network, signedTx);
Flow 9: Disaster Recovery (Portable Key Lost, 2-of-3)
When Portable Key backup is unavailable but user has Silicon Key + Guardian:
// 1. Authenticate to Guardian (via guardian-gated recovery if passkey lost)
// 2. Connect Silicon Key
await sdk.mpc.connectToDevice(deviceId);
// 3. Initiate disaster recovery (resharing with Silicon + Guardian)
final result = await sdk.performDisasterRecovery(groupId);
// 4. MANDATORY: Create new backups for all keys
await sdk.backupService.createBackup(
groupId: wallet.groupId,
wrappers: [...],
);
Flow 10: Backup to Cloud (Auto-sync)
// Default - auto-syncs to iCloud/Google Backup
await sdk.backupService.createBackup(
groupId: wallet.groupId,
wrappers: [
WrapperConfig.passkeyPrf(credentialId: credential.id),
],
storageLocations: [StorageLocation.cloud],
);
// Ensure system backup is enabled
final isEnabled = await sdk.backupService.isSystemBackupEnabled();
if (!isEnabled) {
await sdk.backupService.openSystemBackupSettings();
}
Flow 11: Restore from Cloud (New Device)
// After iCloud/Google sync completes on new device
final hasBackup = await sdk.backupService.hasBackup();
if (hasBackup) {
final info = await sdk.backupService.getBackupInfo();
// Authenticate with synced passkey
await sdk.passkeyService.authenticate();
// Restore
await sdk.backupService.restore(
strategy: UnlockStrategy.passkeyPrf(
credentialId: info.wrappers.first.credentialId!,
),
fromLocation: StorageLocation.cloud,
);
}
Flow 12: Restore with Fallback Sources
// Try cloud first, fall back to imported file
final hasCloudBackup = await sdk.backupService.hasBackup();
if (hasCloudBackup) {
await sdk.backupService.restore(
strategy: UnlockStrategy.passkeyPrf(credentialId: credentialId),
fromLocation: StorageLocation.cloud,
);
} else {
// Prompt user to import backup file
final filePath = await pickBackupFile();
await sdk.backupService.importFromFile(filePath);
await sdk.backupService.restore(
strategy: UnlockStrategy.passkeyPrf(credentialId: credentialId),
);
}
Flow 13: Multiple Passkey Management
// 1. Register additional passkeys for redundancy
final passkey2 = await sdk.passkeyService.register(displayName: "MacBook");
final passkey3 = await sdk.passkeyService.register(displayName: "YubiKey");
// 2. List all credentials
final credentials = await sdk.credentialService.getCredentials();
// 3. Add all PRF-capable passkeys as wrappers
final prfCapable = await sdk.credentialService.getPrfCapableCredentials();
final wrappers = prfCapable
.map((c) => WrapperConfig.passkeyPrf(credentialId: c.id))
.toList();
await sdk.backupService.createBackup(
groupId: wallet.groupId,
wrappers: wrappers,
);
// 4. Set default credential for authentication
await sdk.credentialService.setDefaultCredential(passkey2.id);
// 5. Revoke lost credential and remove from backup
await sdk.credentialService.revokeCredential(passkey1.id);
await sdk.backupService.removeWrapper(wrapIdForPasskey1);
Flow 14: Add PIN as Fallback Recovery
// Add PIN wrapper to existing backup
await sdk.backupService.addWrapper(
WrapperConfig.pin("123456"),
);
// Now backup has both PRF and PIN wrappers
// User can restore with either
Flow 15: Backup to Both Cloud and Export
// Advanced users might want both
await sdk.backupService.createBackup(
groupId: wallet.groupId,
wrappers: [
WrapperConfig.passkeyPrf(credentialId: credential.id),
],
storageLocations: [
StorageLocation.cloud,
StorageLocation.export,
],
);
// Export a copy for manual storage
final exportPath = await sdk.backupService.exportToDownloads();
Flow 16: Share Refresh (Security Hygiene)
After device revocation or periodic refresh (180 days):
// 1. Perform share refresh protocol
await sdk.performShareRefresh(groupId);
// 2. MANDATORY: Old backup is now invalid - create new one
await sdk.backupService.createBackup(
groupId: wallet.groupId,
wrappers: [
WrapperConfig.passkeyPrf(credentialId: credential.id),
],
);
Flow 17: Import Backup from File
// User selects exported backup file
final filePath = await pickBackupFile();
// Import into local storage
await sdk.backupService.importFromFile(filePath);
// Restore with credential
final info = await sdk.backupService.getBackupInfo();
await sdk.backupService.restore(
strategy: UnlockStrategy.passkeyPrf(
credentialId: info.wrappers.first.credentialId!,
),
);
Recovery Matrix
Portable Key Recovery Scenarios
| Backup Artifacts | Passkey Available | Passkey Lost (Novice) | Passkey Lost (Normal/Advanced) |
|---|---|---|---|
| PRF + Guardian-Gated | ✅ Use PRF | ✅ Gate → time-lock → new passkey → KEK | ❌ Irrecoverable |
| Guardian-Gated Only | ✅ Request KEK | ✅ Gate → time-lock → new passkey → KEK | ❌ Irrecoverable |
| PRF Only | ✅ Use PRF | ❌ Irrecoverable | ❌ Irrecoverable |
| PIN/Password | ✅ Use PIN/Password | ✅ Use PIN/Password | ✅ Use PIN/Password |
Disaster Recovery (2-of-3 Only)
| Scenario | Silicon Key Available | Silicon Key Lost (NFC Available) | Silicon Key Lost (No NFC) |
|---|---|---|---|
| Can auth to Guardian | ✅ Disaster recovery | ✅ Recover Silicon via NFC → Disaster recovery | ❌ Irrecoverable |
| Can't auth (Novice) | ✅ Gate recovery → Disaster recovery | ✅ Gate recovery → NFC → Disaster recovery | ❌ Irrecoverable |
| Can't auth (Normal) | ❌ Irrecoverable | ❌ Irrecoverable | ❌ Irrecoverable |
Security Considerations
Key Principles
- Explicit credential IDs: No auto-expansion of
allPasskeys()to prevent accidental PRF operations - PRF output never exposed: Internal to SDK, never visible to Flutter
- KEK ephemeral: Never persisted, only derived when needed
- DEK random per backup: New DEK generated on each backup creation
Wrapper Security Levels
| Wrapper Type | KEK Derivation | Security Level | Platform Support |
|---|---|---|---|
passkeyPrf | HKDF(PRF_output, salt) | Highest | iOS 18+, Android 14+ |
guardianGatedEscrow | Random KEK (escrowed) | Medium | All |
pin | PBKDF2(pin, salt, 600k) | Medium | All |
password | PBKDF2(password, salt, 600k) | Medium-High | All |
none | N/A | None (dev only) | All |
Platform Support
PRF Support Matrix
| Platform | Minimum Version | PRF Supported |
|---|---|---|
| iOS | 18.0+ | ✅ |
| iOS | < 18.0 | ❌ |
| Android | 14+ (API 34) | ✅ |
| Android | < 14 | ❌ |
Storage Locations
| Location | iOS | Android |
|---|---|---|
cloud | Documents/ (iCloud synced) | filesDir/ (Google Backup synced) |
export | Downloads/ | Downloads/ |
nfcCard | Coming soon | Coming soon |
Implementation Status
| Service | API Status | Backend Status | Notes |
|---|---|---|---|
| PasskeyService | ✅ Complete | ✅ Ready | Full WebAuthn ceremonies |
| CredentialService | ✅ Complete | N/A (local) | Local registry management |
| BackupService | ✅ Complete | N/A (local) | V3 bundle format |
| RecoveryService | ✅ API Complete | ⏳ Backend Pending | Requires Guardian backend |
Manual Testing Required: All services need manual testing on iOS 18+ and Android 14+ devices before production release. See Phase 6 changelog for test plan.
Coming Soon
Features designed but not yet fully available:
- RecoveryService backend - API implemented, Guardian backend integration pending
- StorageLocation.nfcCard - NFC backup card support
- Direct cloud API - Currently uses system backup sync