diff --git a/lib/packages/repository/wallet_repository.dart b/lib/packages/repository/wallet_repository.dart index 13b9d582..e2ffbce7 100644 --- a/lib/packages/repository/wallet_repository.dart +++ b/lib/packages/repository/wallet_repository.dart @@ -44,6 +44,14 @@ class WalletRepository { Future deleteWallet(int id) => _appDatabase.deleteWallet(id); + /// Full purge for the user-facing delete: removes the encrypted-seed row AND + /// the AES-GCM mnemonic key, so no recoverable seed material remains on + /// device. + Future purgeWallet(int id) async { + await _appDatabase.deleteWalletCompletely(id); + await _secureStorage.deleteMnemonicKey(); + } + Future _decryptWalletInfo(WalletInfo info) async { final key = await _secureStorage.getOrCreateMnemonicKey(); final decryptedSeed = SecureStorage.decryptSeed(key, info.seed); diff --git a/lib/packages/service/wallet_service.dart b/lib/packages/service/wallet_service.dart index 02322e93..a0a0fa70 100644 --- a/lib/packages/service/wallet_service.dart +++ b/lib/packages/service/wallet_service.dart @@ -274,7 +274,9 @@ class WalletService { Future deleteCurrentWallet() async { final id = _settingsRepository.currentWalletId!; - await _repository.deleteWallet(id); + // Full purge (seed row + mnemonic key), not an account-only delete — so no + // recoverable seed survives delete. + await _repository.purgeWallet(id); await _settingsRepository.removeCurrentWalletId(); } diff --git a/lib/packages/storage/secure_storage.dart b/lib/packages/storage/secure_storage.dart index 8472bae4..55c78b27 100644 --- a/lib/packages/storage/secure_storage.dart +++ b/lib/packages/storage/secure_storage.dart @@ -165,6 +165,14 @@ class SecureStorage { return key; } + /// Removes the AES-GCM key that decrypts stored seeds. Once gone, any + /// surviving encrypted seed is permanently undecryptable; a fresh key is + /// lazily minted on next creation. + // @no-integration-test: forwards to FlutterSecureStorage (Android Keystore / + // iOS Keychain) over a platform channel; real keystore removal is only + // verifiable on-device — the unit test mocks the plugin. + Future deleteMnemonicKey() => _secureStorage.delete(key: _mnemonicEncryptionKey); + static String encryptSeed(Uint8List key, String plaintext) { final iv = _secureRandomBytes(12); final cipher = GCMBlockCipher(AESEngine()) diff --git a/lib/packages/storage/wallet_storage.dart b/lib/packages/storage/wallet_storage.dart index 5eb8c83a..81317d76 100644 --- a/lib/packages/storage/wallet_storage.dart +++ b/lib/packages/storage/wallet_storage.dart @@ -28,6 +28,14 @@ extension WalletStorage on AppDatabase { Future deleteWallet(int walletId) => (delete(walletAccountInfos)..where((row) => row.wallet.equals(walletId))).go(); + /// Deletes the `walletInfos` row itself (the encrypted-seed record) after + /// clearing its dependent `walletAccountInfos` rows (FK in + /// [WalletAccountInfos.wallet]). + Future deleteWalletCompletely(int walletId) => transaction(() async { + await (delete(walletAccountInfos)..where((row) => row.wallet.equals(walletId))).go(); + await (delete(walletInfos)..where((row) => row.id.equals(walletId))).go(); + }); + Future get hasWallet => select(walletInfos).get().then((result) => result.isNotEmpty); } diff --git a/test/packages/repository/wallet_repository_test.dart b/test/packages/repository/wallet_repository_test.dart index f049bca4..b9ab2ded 100644 --- a/test/packages/repository/wallet_repository_test.dart +++ b/test/packages/repository/wallet_repository_test.dart @@ -145,5 +145,34 @@ void main() { final afterAccounts = await db.getWalletAccounts(walletId); expect(afterAccounts, isEmpty); }); + + test('purgeWallet removes the walletInfos seed row AND the mnemonic key', () async { + // The user-facing delete must leave no recoverable seed material — + // neither the encrypted row nor the AES key. + when(() => secureStorage.deleteMnemonicKey()).thenAnswer((_) async {}); + + final walletId = await repo.createWallet(walletName, WalletType.software, seed, address); + await db.insertWalletAccount(walletId, 'acc-0', 0); + expect(await db.getWalletById(walletId), isNotNull); + + await repo.purgeWallet(walletId); + + expect(await db.getWalletById(walletId), isNull); // encrypted seed row gone + expect(await db.getWalletAccounts(walletId), isEmpty); // accounts gone + verify(() => secureStorage.deleteMnemonicKey()).called(1); // AES key removed + }); + + test('deleteWallet (account-only) leaves the seed row and mnemonic key intact', () async { + // Onboarding-regenerate contract: the account-only primitive must NOT + // wipe the seed row or the AES key. + final walletId = await repo.createWallet(walletName, WalletType.software, seed, address); + await db.insertWalletAccount(walletId, 'acc-0', 0); + + await repo.deleteWallet(walletId); + + expect(await db.getWalletById(walletId), isNotNull); // row survives + expect(await db.getWalletAccounts(walletId), isEmpty); // accounts gone + verifyNever(() => secureStorage.deleteMnemonicKey()); // key untouched + }); }); } diff --git a/test/packages/service/wallet_service_test.dart b/test/packages/service/wallet_service_test.dart index 91ea64d9..24f19cfa 100644 --- a/test/packages/service/wallet_service_test.dart +++ b/test/packages/service/wallet_service_test.dart @@ -57,6 +57,7 @@ void main() { when(() => settings.saveCurrentWalletId(any())).thenAnswer((_) async => true); when(() => settings.removeCurrentWalletId()).thenAnswer((_) async => true); when(() => repo.deleteWallet(any())).thenAnswer((_) async {}); + when(() => repo.purgeWallet(any())).thenAnswer((_) async {}); when(() => repo.updateAddress(any(), any())).thenAnswer((_) async {}); }); @@ -440,7 +441,10 @@ void main() { await service.deleteCurrentWallet(); - verify(() => repo.deleteWallet(8)).called(1); + // User-facing delete must fully purge (seed row + mnemonic key), not + // the account-only delete. + verify(() => repo.purgeWallet(8)).called(1); + verifyNever(() => repo.deleteWallet(any())); verify(() => settings.removeCurrentWalletId()).called(1); }); }); diff --git a/test/packages/storage/secure_storage_test.dart b/test/packages/storage/secure_storage_test.dart index 19508ca6..96df3098 100644 --- a/test/packages/storage/secure_storage_test.dart +++ b/test/packages/storage/secure_storage_test.dart @@ -120,6 +120,12 @@ void main() { verify(() => mockStorage.delete(key: 'pin.salt')).called(1); }); + test('deleteMnemonicKey deletes the mnemonic encryption key', () async { + await secureStorage.deleteMnemonicKey(); + + verify(() => mockStorage.delete(key: 'wallet.mnemonic.encryption.key')).called(1); + }); + test('getPinSalt returns null when no salt is stored', () async { when( () => mockStorage.read(key: 'pin.salt'),