Skip to main content

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:

TierGuardian-Gated RecoveryKEK StorageBackup Location
Novice✅ EnabledServer (escrowed)Cloud (auto)
Normal❌ DisabledLocal onlyCloud (auto)
Advanced❌ DisabledLocal onlyManual 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 ArtifactsPasskey AvailablePasskey Lost (Novice)Passkey Lost (Normal/Advanced)
PRF + Guardian-Gated✅ Use PRF✅ Gate → time-lock → new passkey → KEKIrrecoverable
Guardian-Gated Only✅ Request KEK✅ Gate → time-lock → new passkey → KEKIrrecoverable
PRF Only✅ Use PRFIrrecoverableIrrecoverable
PIN/Password✅ Use PIN/Password✅ Use PIN/Password✅ Use PIN/Password

Disaster Recovery (2-of-3 Only)

ScenarioSilicon Key AvailableSilicon Key Lost (NFC Available)Silicon Key Lost (No NFC)
Can auth to Guardian✅ Disaster recovery✅ Recover Silicon via NFC → Disaster recoveryIrrecoverable
Can't auth (Novice)✅ Gate recovery → Disaster recovery✅ Gate recovery → NFC → Disaster recoveryIrrecoverable
Can't auth (Normal)IrrecoverableIrrecoverableIrrecoverable

Security Considerations

Key Principles

  1. Explicit credential IDs: No auto-expansion of allPasskeys() to prevent accidental PRF operations
  2. PRF output never exposed: Internal to SDK, never visible to Flutter
  3. KEK ephemeral: Never persisted, only derived when needed
  4. DEK random per backup: New DEK generated on each backup creation

Wrapper Security Levels

Wrapper TypeKEK DerivationSecurity LevelPlatform Support
passkeyPrfHKDF(PRF_output, salt)HighestiOS 18+, Android 14+
guardianGatedEscrowRandom KEK (escrowed)MediumAll
pinPBKDF2(pin, salt, 600k)MediumAll
passwordPBKDF2(password, salt, 600k)Medium-HighAll
noneN/ANone (dev only)All

Platform Support

PRF Support Matrix

PlatformMinimum VersionPRF Supported
iOS18.0+
iOS< 18.0
Android14+ (API 34)
Android< 14

Storage Locations

LocationiOSAndroid
cloudDocuments/ (iCloud synced)filesDir/ (Google Backup synced)
exportDownloads/Downloads/
nfcCardComing soonComing soon

Implementation Status

ServiceAPI StatusBackend StatusNotes
PasskeyService✅ Complete✅ ReadyFull WebAuthn ceremonies
CredentialService✅ CompleteN/A (local)Local registry management
BackupService✅ CompleteN/A (local)V3 bundle format
RecoveryService✅ API Complete⏳ Backend PendingRequires 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:

  1. RecoveryService backend - API implemented, Guardian backend integration pending
  2. StorageLocation.nfcCard - NFC backup card support
  3. Direct cloud API - Currently uses system backup sync

References