diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7b9ba35269..0bb0b16d36 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -36,6 +36,8 @@ { @@ -12,7 +14,18 @@ abstract class HiveCacheClient { bool get encryption => false; - Future _getEncryptionKey() => HiveCacheConfig.instance.getEncryptionKey(); + Future _getEncryptionKey() async { + try { + if (PlatformInfo.isAndroid) { + return await AppSecurityManager.instance.getKey(); + } else { + return await HiveCacheConfig.instance.getEncryptionKey(); + } + } catch (e) { + logWarning('$runtimeType::_getEncryptionKey: Exception $e'); + return null; + } + } Future> openIsolatedBox() async { if (IsolatedHive.isBoxOpen(tableName)) { @@ -20,10 +33,16 @@ abstract class HiveCacheClient { } else { if (encryption) { final encryptionKey = await _getEncryptionKey(); + + if (encryptionKey == null) { + throw Exception( + '$runtimeType: Encryption enabled but key is null', + ); + } + return IsolatedHive.openBox( tableName, - encryptionCipher: - encryptionKey != null ? HiveAesCipher(encryptionKey) : null, + encryptionCipher: HiveAesCipher(encryptionKey), ); } else { return IsolatedHive.openBox(tableName); @@ -37,10 +56,16 @@ abstract class HiveCacheClient { } else { if (encryption) { final encryptionKey = await _getEncryptionKey(); + + if (encryptionKey == null) { + throw Exception( + '$runtimeType: Encryption enabled but key is null', + ); + } + return Hive.openBox( tableName, - encryptionCipher: - encryptionKey != null ? HiveAesCipher(encryptionKey) : null, + encryptionCipher: HiveAesCipher(encryptionKey), ); } else { return Hive.openBox(tableName); @@ -337,5 +362,9 @@ abstract class HiveCacheClient { await Hive.box(tableName).close(); } } + + if (PlatformInfo.isAndroid) { + AppSecurityManager.instance.clearKey(); + } } } diff --git a/lib/features/caching/config/secure_storage_factory.dart b/lib/features/caching/config/secure_storage_factory.dart new file mode 100644 index 0000000000..c1bbb62d5d --- /dev/null +++ b/lib/features/caching/config/secure_storage_factory.dart @@ -0,0 +1,16 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:tmail_ui_user/main/utils/app_config.dart'; + +class SecureStorageFactory { + const SecureStorageFactory._(); + + static FlutterSecureStorage create() { + return const FlutterSecureStorage( + iOptions: IOSOptions( + groupId: AppConfig.iOSKeychainSharingGroupId, + accountName: AppConfig.iOSKeychainSharingService, + accessibility: KeychainAccessibility.first_unlock_this_device, + ), + ); + } +} diff --git a/lib/features/caching/config/secure_storage_keys.dart b/lib/features/caching/config/secure_storage_keys.dart new file mode 100644 index 0000000000..43564d0b1d --- /dev/null +++ b/lib/features/caching/config/secure_storage_keys.dart @@ -0,0 +1,5 @@ +class SecureStorageKeys { + const SecureStorageKeys._(); + + static const String hiveEncryptionKey = 'hive_encryption_key'; +} diff --git a/lib/features/caching/manager/app_security_manager.dart b/lib/features/caching/manager/app_security_manager.dart new file mode 100644 index 0000000000..ae83b08def --- /dev/null +++ b/lib/features/caching/manager/app_security_manager.dart @@ -0,0 +1,196 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:core/utils/app_logger.dart'; +import 'package:hive_ce/hive.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tmail_ui_user/features/caching/config/secure_storage_factory.dart'; +import 'package:tmail_ui_user/features/caching/config/secure_storage_keys.dart'; + +class AppSecurityManager { + static final instance = AppSecurityManager._(); + + AppSecurityManager._(); + + final _storage = SecureStorageFactory.create(); + + Uint8List? _cachedKey; + + Future init() async { + log('AppSecurityManager::init: Start initialization'); + + try { + final storedKey = await _readStoredKey(); + final hiveExists = await _doesHiveExistSafe(); + + await _handleInconsistentState( + hiveExists: hiveExists, + storedKey: storedKey, + ); + + await _ensureKeyExists(storedKey); + + log('AppSecurityManager::init: Initialization completed'); + } catch (e) { + logWarning( + 'AppSecurityManager::init: Initialization failed, Exception $e', + ); + } + } + + Future getKey() async { + if (_cachedKey != null) { + log('AppSecurityManager::getKey: Returning cached key'); + return _cachedKey; + } + + try { + final key = await _loadKeySafe(); + _cachedKey = key; + return key; + } catch (e, st) { + logError( + 'AppSecurityManager::getKey: Failed to load key', + exception: e, + stackTrace: st, + ); + return null; + } + } + + void clearKey() { + log('AppSecurityManager::clearKey: Clearing cached key'); + _cachedKey = null; + } + + Future _readStoredKey() async { + try { + final key = await _storage.read( + key: SecureStorageKeys.hiveEncryptionKey, + ); + log('AppSecurityManager::_readStoredKey: Key exists: ${key != null}'); + return key; + } catch (e) { + logWarning( + 'AppSecurityManager::_readStoredKey: Read failed, Exception $e', + ); + return null; + } + } + + Future _handleInconsistentState({ + required bool hiveExists, + required String? storedKey, + }) async { + if (hiveExists && storedKey == null) { + logWarning( + 'AppSecurityManager::_handleInconsistentState: ' + 'Hive exists but key missing → wiping Hive, SharePreference', + ); + await _wipeLocalStorageSafe(); + } + } + + Future _ensureKeyExists(String? storedKey) async { + if (storedKey != null) { + log('AppSecurityManager::_ensureKeyExists: Key already exists'); + return; + } + + log('AppSecurityManager::_ensureKeyExists: Generating new key'); + + try { + final newKey = Hive.generateSecureKey(); + + await _storage.write( + key: SecureStorageKeys.hiveEncryptionKey, + value: base64UrlEncode(newKey), + ); + + log('AppSecurityManager::_ensureKeyExists: Key stored successfully'); + } catch (e, st) { + logError( + 'AppSecurityManager::_ensureKeyExists: Failed to store key', + exception: e, + stackTrace: st, + ); + rethrow; + } + } + + Future _loadKeySafe() async { + final key = await _readStoredKey(); + + if (key == null) { + throw StateError('Encryption key not found'); + } + + try { + return base64Url.decode(key); + } catch (e, st) { + logError( + 'AppSecurityManager::_loadKeySafe: Decode failed', + exception: e, + stackTrace: st, + ); + throw StateError('Invalid encryption key format'); + } + } + + Future _doesHiveExistSafe() async { + try { + final dir = await getApplicationDocumentsDirectory(); + final files = Directory(dir.path).listSync(); + + final exists = files.any((f) => f.path.endsWith('.hive')); + + log('AppSecurityManager::_doesHiveExistSafe: Exists = $exists'); + return exists; + } catch (e) { + logWarning( + 'AppSecurityManager::_doesHiveExistSafe: Check failed, Exception $e', + ); + return false; + } + } + + Future _wipeLocalStorageSafe() async { + try { + logWarning('AppSecurityManager::_wipeLocalStorageSafe: Deleting local storage data'); + await Future.wait([ + _wipeHiveSafe(), + _wipeSharePreferenceSafe(), + ]); + } catch (e, st) { + logError( + 'AppSecurityManager::_wipeLocalStorageSafe: Delete failed', + exception: e, + stackTrace: st, + ); + rethrow; + } + } + + Future _wipeHiveSafe() async { + try { + logWarning('AppSecurityManager::_wipeHiveSafe: Deleting Hive data'); + await Hive.deleteFromDisk(); + } catch (e) { + logWarning('AppSecurityManager::_wipeHiveSafe: Delete failed, Exception $e'); + rethrow; + } + } + + Future _wipeSharePreferenceSafe() async { + try { + logWarning('AppSecurityManager::_wipeSharePreferenceSafe: Deleting SharePreference data'); + final sharedPreferences = await SharedPreferences.getInstance(); + await sharedPreferences.clear(); + } catch (e) { + logWarning('AppSecurityManager::_wipeSharePreferenceSafe: Delete failed, Exception $e'); + rethrow; + } + } +} diff --git a/lib/main/bindings/core/core_bindings.dart b/lib/main/bindings/core/core_bindings.dart index 7523caaf3f..273e5ea914 100644 --- a/lib/main/bindings/core/core_bindings.dart +++ b/lib/main/bindings/core/core_bindings.dart @@ -9,15 +9,14 @@ import 'package:core/utils/platform_info.dart'; import 'package:core/utils/preview_eml_file_utils.dart'; import 'package:core/utils/print_utils.dart'; import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tmail_ui_user/features/base/before_reconnect_manager.dart'; +import 'package:tmail_ui_user/features/caching/config/secure_storage_factory.dart'; import 'package:tmail_ui_user/features/caching/utils/local_storage_manager.dart'; import 'package:tmail_ui_user/features/caching/utils/session_storage_manager.dart'; import 'package:tmail_ui_user/features/sending_queue/presentation/utils/sending_queue_isolate_manager.dart'; import 'package:tmail_ui_user/main/permissions/permission_service.dart'; -import 'package:tmail_ui_user/main/utils/app_config.dart'; import 'package:tmail_ui_user/main/utils/email_receive_manager.dart'; import 'package:tmail_ui_user/main/utils/ios_notification_manager.dart'; import 'package:tmail_ui_user/main/utils/toast_manager.dart'; @@ -87,13 +86,7 @@ class CoreBindings extends Bindings { } void _bindingStorage() { - Get.put(const FlutterSecureStorage( - iOptions: IOSOptions( - groupId: AppConfig.iOSKeychainSharingGroupId, - accountName: AppConfig.iOSKeychainSharingService, - accessibility: KeychainAccessibility.first_unlock_this_device - ) - )); + Get.put(SecureStorageFactory.create()); Get.put(LocalStorageManager()); Get.put(SessionStorageManager()); } diff --git a/lib/main/main_entry.dart b/lib/main/main_entry.dart index c251152f44..b4eb48f8d7 100644 --- a/lib/main/main_entry.dart +++ b/lib/main/main_entry.dart @@ -5,6 +5,7 @@ import 'package:core/utils/platform_info.dart'; import 'package:flutter/widgets.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; +import 'package:tmail_ui_user/features/caching/manager/app_security_manager.dart'; import 'package:tmail_ui_user/main.dart'; import 'package:tmail_ui_user/main/bindings/main_bindings.dart'; import 'package:tmail_ui_user/main/utils/asset_preloader.dart'; @@ -20,6 +21,10 @@ Future runTmail() async { Future runTmailPreload() async { ThemeUtils.setSystemLightUIStyle(); + if (PlatformInfo.isAndroid) { + await AppSecurityManager.instance.init(); + } + await Future.wait([ MainBindings().dependencies(), HiveCacheConfig.instance.setUp(), @@ -29,7 +34,10 @@ Future runTmailPreload() async { await Get.find().warmUp(log: BuildUtils.isDebugMode); await CozyIntegration.integrateCozy(); - await HiveCacheConfig.instance.initializeEncryptionKey(); + + if (!PlatformInfo.isAndroid) { + await HiveCacheConfig.instance.initializeEncryptionKey(); + } if (PlatformInfo.isWeb) { setPathUrlStrategy();