'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.
51 lines
1.8 KiB
Dart
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();
|
|
}
|