From da56ed8f5227d07372e963546d779e3f64442d2f Mon Sep 17 00:00:00 2001 From: Blazebrain Date: Tue, 24 Jun 2025 17:17:15 +0100 Subject: [PATCH 1/2] feat(ios-storage-issue): Remove 2FA/PIN persistence across app re-installs in iOS This error occurs due to the keychain persisting auth data on user devices connected to the same iCloud --- lib/core/reset_service.dart | 125 ++++++++++++++++++++++++++++++++++++ lib/di.dart | 9 +++ lib/main.dart | 11 ++-- 3 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 lib/core/reset_service.dart diff --git a/lib/core/reset_service.dart b/lib/core/reset_service.dart new file mode 100644 index 000000000..8138a510d --- /dev/null +++ b/lib/core/reset_service.dart @@ -0,0 +1,125 @@ +import 'package:cake_wallet/core/secure_storage.dart'; +import 'package:cake_wallet/entities/secret_store_key.dart'; +import 'package:cake_wallet/store/authentication_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:cw_core/utils/print_verbose.dart'; + +class ResetService { + ResetService({ + required this.secureStorage, + required this.authenticationStore, + required this.settingsStore, + }); + + final SecureStorage secureStorage; + final AuthenticationStore authenticationStore; + final SettingsStore settingsStore; + + static const List _authKeys = [ + SecureKey.allowBiometricalAuthenticationKey, + SecureKey.useTOTP2FA, + SecureKey.shouldRequireTOTP2FAForAccessingWallet, + SecureKey.shouldRequireTOTP2FAForSendsToContact, + SecureKey.shouldRequireTOTP2FAForSendsToNonContact, + SecureKey.shouldRequireTOTP2FAForSendsToInternalWallets, + SecureKey.shouldRequireTOTP2FAForExchangesToInternalWallets, + SecureKey.shouldRequireTOTP2FAForExchangesToExternalWallets, + SecureKey.shouldRequireTOTP2FAForAddingContacts, + SecureKey.shouldRequireTOTP2FAForCreatingNewWallets, + SecureKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + SecureKey.selectedCake2FAPreset, + SecureKey.totpSecretKey, + SecureKey.pinTimeOutDuration, + SecureKey.lastAuthTimeMilliseconds, + 'PIN_CODE_PASSWORD', + ]; + + bool _isAuthKey(String key) => _authKeys.contains(key); + + /// Checks if this is a new install and clears any existing auth data from Keychain + Future resetAuthDataOnNewInstall(SharedPreferences sharedPreferences) async { + try { + final isNewInstall = sharedPreferences.getBool(PreferencesKey.isNewInstall) ?? false; + + if (isNewInstall) { + await _clearExistingAuthDataOnNewInstall(); + } + } catch (e) { + printV('Error during new install auth reset: $e'); + } + } + + /// Checks if there's existing auth data that should be cleared on new install + Future _clearExistingAuthDataOnNewInstall() async { + final allKeys = await secureStorage.readAll(); + final authKeysFound = []; + + for (final key in allKeys.keys) { + if (_isAuthKey(key)) { + authKeysFound.add(key); + } + } + + if (authKeysFound.isNotEmpty) { + printV( + 'Found ${authKeysFound.length} existing auth keys in storage: ${authKeysFound.join(', ')}', + ); + + await resetAuthenticationData(); + } + } + + /// Resets authentication data from both secure storage and settings store + Future resetAuthenticationData() async { + try { + await Future.wait([ + _deleteAuthenticationKeys(), + _resetSettingsStoreAuthData(), + ]); + } catch (e) { + printV('An error occurred during authentication reset: $e'); + rethrow; + } + } + + /// Resets authentication-related data in SettingsStore to default values + Future _resetSettingsStoreAuthData() async { + settingsStore.useTOTP2FA = false; + settingsStore.totpSecretKey = ''; + settingsStore.shouldRequireTOTP2FAForAccessingWallet = false; + settingsStore.shouldRequireTOTP2FAForSendsToContact = false; + settingsStore.shouldRequireTOTP2FAForSendsToNonContact = false; + settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets = false; + settingsStore.shouldRequireTOTP2FAForExchangesToInternalWallets = false; + settingsStore.shouldRequireTOTP2FAForExchangesToExternalWallets = false; + settingsStore.shouldRequireTOTP2FAForAddingContacts = false; + settingsStore.shouldRequireTOTP2FAForCreatingNewWallets = false; + settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings = false; + settingsStore.allowBiometricalAuthentication = false; + } + + /// Deletes authentication keys from secure storage + Future _deleteAuthenticationKeys() async { + final failedDeletions = []; + + final deletionFutures = _authKeys.map((key) async { + try { + await secureStorage.delete(key: key); + } catch (e) { + failedDeletions.add(key); + } + }); + + await Future.wait(deletionFutures); + + if (failedDeletions.isNotEmpty) { + printV( + 'Warning: Failed to delete ${failedDeletions.length} auth keys: ${failedDeletions.join(', ')}', + ); + } else { + printV('All auth keys deleted successfully'); + } + } +} diff --git a/lib/di.dart b/lib/di.dart index 5a7be0f1f..35a7e670a 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -280,6 +280,7 @@ import 'cake_pay/cake_pay_payment_credantials.dart'; import 'package:cake_wallet/view_model/dev/background_sync_logs_view_model.dart'; import 'package:cake_wallet/src/screens/dev/background_sync_logs_page.dart'; import 'package:cake_wallet/core/trade_monitor.dart'; +import 'package:cake_wallet/core/reset_service.dart'; final getIt = GetIt.instance; @@ -557,6 +558,14 @@ Future setup({ ), ); + getIt.registerFactory( + () => ResetService( + secureStorage: getIt.get(), + authenticationStore: getIt.get(), + settingsStore: getIt.get(), + ), + ); + getIt.registerFactory(() => AuthViewModel(getIt.get(), getIt.get(), getIt.get(), BiometricAuth())); diff --git a/lib/main.dart b/lib/main.dart index 709304939..e48ea5cc2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -25,7 +25,6 @@ import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/root/root.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/authentication_store.dart'; -import 'package:cake_wallet/themes/core/material_base_theme.dart'; import 'package:cake_wallet/themes/utils/theme_provider.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/device_info.dart'; @@ -56,6 +55,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:cw_core/window_size.dart'; import 'package:logging/logging.dart'; import 'package:cake_wallet/core/trade_monitor.dart'; +import 'package:cake_wallet/core/reset_service.dart'; final navigatorKey = GlobalKey(); final rootKey = GlobalKey(); @@ -202,8 +202,8 @@ Future initializeAppConfigs({bool loadWallet = true}) async { final powNodes = await CakeHive.openBox(Node.boxName + "pow"); // must be different from Node.boxName final transactionDescriptions = await CakeHive.openBox( - TransactionDescription.boxName, - encryptionKey: transactionDescriptionsBoxKey); + TransactionDescription.boxName, + encryptionKey: transactionDescriptionsBoxKey); final trades = await CakeHive.openBox(Trade.boxName, encryptionKey: tradesBoxKey); final orders = await CakeHive.openBox(Order.boxName, encryptionKey: ordersBoxKey); final walletInfoSource = await CakeHive.openBox(WalletInfo.boxName); @@ -287,6 +287,9 @@ Future initialSetup({ navigatorKey: navigatorKey, secureStorage: secureStorage, ); + + getIt.get().resetAuthDataOnNewInstall(sharedPreferences); + await bootstrapOffline(); final settingsStore = getIt(); if (!settingsStore.currentBuiltinTor) { @@ -314,7 +317,7 @@ class AppState extends State with SingleTickerProviderStateMixin { final statusBarColor = Colors.transparent; final authenticationStore = getIt.get(); final initialRoute = authenticationStore.state == AuthenticationState.uninitialized - ? Routes.welcome + ? Routes.welcome : settingsStore.currentBuiltinTor ? Routes.startTor : Routes.login; final currentTheme = appStore.themeStore.currentTheme; final statusBarBrightness = currentTheme.type == currentTheme.isDark From b5b4878d624b0d40300bbb14040b52b34a6f39b3 Mon Sep 17 00:00:00 2001 From: Blazebrain Date: Wed, 25 Jun 2025 06:02:07 +0100 Subject: [PATCH 2/2] feat(reset-service): Remove PIN persistence for app reinstall --- lib/core/reset_service.dart | 53 +++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/lib/core/reset_service.dart b/lib/core/reset_service.dart index 8138a510d..94df9ca9e 100644 --- a/lib/core/reset_service.dart +++ b/lib/core/reset_service.dart @@ -36,15 +36,20 @@ class ResetService { 'PIN_CODE_PASSWORD', ]; + static const List _walletPreferencesKeys = [ + PreferencesKey.currentWalletName, + PreferencesKey.currentWalletType, + ]; + bool _isAuthKey(String key) => _authKeys.contains(key); - /// Checks if this is a new install and clears any existing auth data from Keychain + /// Checks if this is a new install and clears any existing auth data from Storage Future resetAuthDataOnNewInstall(SharedPreferences sharedPreferences) async { try { final isNewInstall = sharedPreferences.getBool(PreferencesKey.isNewInstall) ?? false; if (isNewInstall) { - await _clearExistingAuthDataOnNewInstall(); + await _clearExistingAuthDataOnNewInstall(sharedPreferences); } } catch (e) { printV('Error during new install auth reset: $e'); @@ -52,9 +57,10 @@ class ResetService { } /// Checks if there's existing auth data that should be cleared on new install - Future _clearExistingAuthDataOnNewInstall() async { + Future _clearExistingAuthDataOnNewInstall(SharedPreferences sharedPreferences) async { final allKeys = await secureStorage.readAll(); final authKeysFound = []; + final walletPrefsFound = []; for (final key in allKeys.keys) { if (_isAuthKey(key)) { @@ -62,21 +68,33 @@ class ResetService { } } - if (authKeysFound.isNotEmpty) { + for (final key in _walletPreferencesKeys) { + if (sharedPreferences.containsKey(key)) { + walletPrefsFound.add(key); + } + } + + if (authKeysFound.isNotEmpty || walletPrefsFound.isNotEmpty) { printV( 'Found ${authKeysFound.length} existing auth keys in storage: ${authKeysFound.join(', ')}', ); + printV( + 'Found ${walletPrefsFound.length} existing wallet preferences: ${walletPrefsFound.join(', ')}', + ); - await resetAuthenticationData(); + await resetAuthenticationData(sharedPreferences); } } - /// Resets authentication data from both secure storage and settings store - Future resetAuthenticationData() async { + /// Resets authentication data auth store, storage and settings store + Future resetAuthenticationData(SharedPreferences sharedPreferences) async { try { + authenticationStore.state = AuthenticationState.uninitialized; + await Future.wait([ _deleteAuthenticationKeys(), _resetSettingsStoreAuthData(), + _clearWalletPreferences(sharedPreferences), ]); } catch (e) { printV('An error occurred during authentication reset: $e'); @@ -100,7 +118,26 @@ class ResetService { settingsStore.allowBiometricalAuthentication = false; } - /// Deletes authentication keys from secure storage + Future _clearWalletPreferences(SharedPreferences sharedPreferences) async { + final failedDeletions = []; + + for (final key in _walletPreferencesKeys) { + try { + await sharedPreferences.remove(key); + } catch (e) { + failedDeletions.add(key); + } + } + + if (failedDeletions.isNotEmpty) { + printV( + 'Warning: Failed to delete ${failedDeletions.length} wallet preferences: ${failedDeletions.join(', ')}', + ); + } else { + printV('All wallet preferences deleted successfully'); + } + } + Future _deleteAuthenticationKeys() async { final failedDeletions = [];