tane/apps/app_seeds/lib/security/secure_key_store.dart
vjrj d6781870d9 feat(backup): sealed backups + printable recovery sheet
'Save a backup' now writes a sealed .tanemaki file (AES-256-GCM under a
key HKDF-derived from the root seed; format TANEBK v1, open and stable).
Restoring on the same identity is silent; a copy sealed by another
identity asks for the printed recovery code (TANE1 base32, typo-tolerant)
and adopts that seed — recovering your bank recovers you. Legacy plain
exports still restore. Settings gains a recovery sheet (QR + code PDF).
BackupBox/RecoveryCode live in commons_core, pure Dart, TDD.
2026-07-09 22:32:13 +02:00

51 lines
1.8 KiB
Dart

import 'package:commons_core/commons_core.dart';
import 'secret_store.dart';
/// Owns the app's secrets in the OS keystore and creates them on first run:
///
/// - the **DB key**: a random 256-bit symmetric key for SQLCipher (NOT derived
/// from any user password — see CLAUDE.md identity section);
/// - the **root seed**: the Duniter/Ğ1-style identity seed (stub for now).
///
/// Both are stored as lowercase hex strings.
class SecureKeyStore {
SecureKeyStore({required SecretStore store, IdentityService? identity})
: _store = store,
_identity = identity ?? IdentityService();
final SecretStore _store;
final IdentityService _identity;
static const dbKeyName = 'tane.db_key';
static const rootSeedName = 'tane.root_seed';
static const _dbKeyLengthBytes = 32;
/// The SQLCipher database key as hex, created and persisted on first access.
Future<String> databaseKeyHex() =>
_readOrCreate(dbKeyName, () => randomBytes(_dbKeyLengthBytes));
/// The root identity seed as hex, created and persisted on first access.
Future<String> rootSeedHex() =>
_readOrCreate(rootSeedName, _identity.generateRootSeed);
/// Replaces the root seed with one recovered from a printed code (restoring
/// your bank on a new device recovers your identity too). The DB key is
/// untouched — it never leaves this device's keystore.
Future<void> adoptRootSeed(String seedHex) =>
_store.write(rootSeedName, seedHex);
Future<String> _readOrCreate(
String key,
List<int> Function() generate,
) async {
final existing = await _store.read(key);
if (existing != null) return existing;
final hex = _toHex(generate());
await _store.write(key, hex);
return hex;
}
static String _toHex(List<int> bytes) =>
bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
}