ensure frost wallet data is fully deleted on wallet delete
What changed, and why it matters
This commit fixes a cleanup bug in Stack Wallet: when a user deleted a FROST (multi-signature) Bitcoin wallet, the app was leaving behind sensitive key material and configuration data in secure storage and the local database. The patch now explicitly deletes those leftover FROST records during wallet deletion, reducing the risk that a deleted wallet's secrets could be recovered or reused later.
Treat this as a security hygiene fix. Users who previously deleted FROST wallets should be advised that stale key material may remain in the app's secure storage and local database until the app is updated and the wallet deletion is re-run or storage is cleared. Review whether any migration or cleanup pass is needed for already-deleted wallets, and consider adding automated tests covering deletion of all wallet-specific secure-storage and database keys.
Security signals we found
Incomplete deletion of cryptographic key material after wallet removal
Residual FROST multisig configuration and recovery data in secure storage and local DB
New cleanup routine added for a specific wallet type, implying prior omission
Deletion of previous-generation key material (PrevGen suffixes)
Evidence from the diff
The change adds a dedicated BitcoinFrostWallet.deleteSecureStorage() helper that removes six FROST-specific secure-storage keys (serializedFROSTKeys, serializedFROSTKeysPrevGen, multisigConfig, multisigConfigPrevGen, multisigIdFROST, recoveryStringFROST) keyed by walletId. It also updates both the public deleteWallet() path and the internal _deleteWallet() path in Wallets to detect FrostCurrency wallets, call the new secure-storage cleanup, and delete the corresponding frostWalletInfo Isar record before removing the walletInfo record. The diff shows the previous code only removed the generic walletInfo entry, leaving FROST data behind.
Changed components
lib/services/wallets.dartlib/wallets/wallet/impl/bitcoin_frost_wallet.dartFROST Bitcoin wallet deletion flowSecureStorageInterface-backed FROST key storageIsar frostWalletInfo database recordsInspect captured patch +50 / −3
diff --git a/lib/services/wallets.dart b/lib/services/wallets.dart
index e1d3814..73ca76a 100644
--- a/lib/services/wallets.dart
+++ b/lib/services/wallets.dart
@@ -24,7 +24,10 @@ import '../utilities/prefs.dart';
import '../utilities/stack_file_system.dart';
import '../wallets/crypto_currency/crypto_currency.dart';
import '../wallets/crypto_currency/intermediate/cryptonote_currency.dart';
+import '../wallets/crypto_currency/intermediate/frost_currency.dart';
+import '../wallets/isar/models/frost_wallet_info.dart';
import '../wallets/isar/models/wallet_info.dart';
+import '../wallets/wallet/impl/bitcoin_frost_wallet.dart';
import '../wallets/wallet/impl/epiccash_wallet.dart';
import '../wallets/wallet/impl/mimblewimblecoin_wallet.dart';
import '../wallets/wallet/intermediate/cryptonote_wallet.dart';
@@ -108,6 +111,7 @@ class Wallets {
SecureStorageInterface secureStorage,
) async {
final walletId = info.walletId;
+ final isFrostWallet = info.coin is FrostCurrency;
Logging.instance.d("deleteWallet called with walletId=$walletId");
final wallet = _wallets[walletId];
@@ -123,6 +127,13 @@ class Wallets {
key: Wallet.getViewOnlyWalletDataSecStoreKey(walletId: walletId),
);
+ if (isFrostWallet) {
+ await BitcoinFrostWallet.deleteSecureStorage(
+ walletId: walletId,
+ secureStorage: secureStorage,
+ );
+ }
+
if (info.coin is CryptonoteCurrency) {
await _deleteCryptonoteWalletFilesHelper(info);
} else if (info.coin is Epiccash) {
@@ -184,6 +195,9 @@ class Wallets {
}
await mainDB.isar.writeTxn(() async {
+ if (isFrostWallet) {
+ await mainDB.isar.frostWalletInfo.deleteByWalletId(walletId);
+ }
await mainDB.isar.walletInfo.deleteByWalletId(walletId);
});
@@ -669,8 +683,24 @@ class Wallets {
Future<void> _deleteWallet(String walletId) async {
// TODO proper clean up of other wallet data in addition to the following
- await mainDB.isar.writeTxn(
- () async => await mainDB.isar.walletInfo.deleteByWalletId(walletId),
- );
+ final info = await mainDB.isar.walletInfo
+ .where()
+ .walletIdEqualTo(walletId)
+ .findFirst();
+ final isFrostWallet =
+ info != null &&
+ AppConfig.getCryptoCurrencyFor(info.coinName) is FrostCurrency;
+ if (isFrostWallet) {
+ await BitcoinFrostWallet.deleteSecureStorage(
+ walletId: walletId,
+ secureStorage: nodeService.secureStorageInterface,
+ );
+ }
+ await mainDB.isar.writeTxn(() async {
+ if (isFrostWallet) {
+ await mainDB.isar.frostWalletInfo.deleteByWalletId(walletId);
+ }
+ await mainDB.isar.walletInfo.deleteByWalletId(walletId);
+ });
}
}
diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart
index 28f53e3..aa34ffa 100644
--- a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart
+++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart
@@ -22,6 +22,7 @@ import '../../../services/event_bus/events/global/wallet_sync_status_changed_eve
import '../../../services/event_bus/global_event_bus.dart';
import '../../../utilities/amount/amount.dart';
import '../../../utilities/extensions/extensions.dart';
+import '../../../utilities/flutter_secure_storage_interface.dart';
import '../../../utilities/logger.dart';
import '../../../wl_gen/interfaces/frost_interface.dart';
import '../../crypto_currency/crypto_currency.dart';
@@ -1112,6 +1113,22 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T>
// =================== Secure storage ========================================
+ static Future<void> deleteSecureStorage({
+ required String walletId,
+ required SecureStorageInterface secureStorage,
+ }) async {
+ for (final suffix in const [
+ 'serializedFROSTKeys',
+ 'serializedFROSTKeysPrevGen',
+ 'multisigConfig',
+ 'multisigConfigPrevGen',
+ 'multisigIdFROST',
+ 'recoveryStringFROST',
+ ]) {
+ await secureStorage.delete(key: '{$walletId}_$suffix');
+ }
+ }
+
Future<String?> getSerializedKeys() async =>
await secureStorageInterface.read(key: "{$walletId}_serializedFROSTKeys");
Why this scored 59/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.