Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
<application
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:fullBackupContent="false"
android:icon="@mipmap/ic_launcher">

<meta-data
Expand Down
39 changes: 34 additions & 5 deletions lib/features/caching/config/hive_cache_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import 'dart:typed_data';

import 'package:core/presentation/extensions/map_extensions.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/platform_info.dart';
import 'package:hive_ce/hive.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/features/caching/utils/cache_utils.dart';

abstract class HiveCacheClient<T> {
Expand All @@ -12,18 +14,35 @@ abstract class HiveCacheClient<T> {

bool get encryption => false;

Future<Uint8List?> _getEncryptionKey() => HiveCacheConfig.instance.getEncryptionKey();
Future<Uint8List?> _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;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have created an interface :

 abstract class EncryptionKeyProvider {
    Future<void> init();
    Future<Uint8List?> getKey();
    void clearKey();
  }

then 2 implem:

class AndroidEncryptionKeyProvider implements EncryptionKeyProvider {
    @override
    Future<void> init() => AppSecurityManager.instance.init();

    @override
    Future<Uint8List?> getKey() => AppSecurityManager.instance.getKey();

    @override
    void clearKey() => AppSecurityManager.instance.clearKey();
  }

class DefaultEncryptionKeyProvider implements EncryptionKeyProvider {
@OverRide
Future init() => HiveCacheConfig.instance.initializeEncryptionKey();

@override
Future<Uint8List?> getKey() => HiveCacheConfig.instance.getEncryptionKey();

@override
void clearKey() {
}

}


and a factory: 

class EncryptionKeyProviderFactory {
const EncryptionKeyProviderFactory._();

static EncryptionKeyProvider create() {
  if (PlatformInfo.isAndroid) {
    return AndroidEncryptionKeyProvider();
  }
  return DefaultEncryptionKeyProvider();
}

}


Like that "no more" `if(Platform...)`. And maybe it'll be easier to add unit tests for this part. Then if tomorrow we want to use the Apple KeyStore, then we can easily add it and etc. 


Future<IsolatedBox<T>> openIsolatedBox() async {
if (IsolatedHive.isBoxOpen(tableName)) {
return IsolatedHive.box<T>(tableName);
} else {
if (encryption) {
final encryptionKey = await _getEncryptionKey();

if (encryptionKey == null) {
throw Exception(
'$runtimeType: Encryption enabled but key is null',
);
}

return IsolatedHive.openBox<T>(
tableName,
encryptionCipher:
encryptionKey != null ? HiveAesCipher(encryptionKey) : null,
encryptionCipher: HiveAesCipher(encryptionKey),
);
} else {
return IsolatedHive.openBox<T>(tableName);
Expand All @@ -37,10 +56,16 @@ abstract class HiveCacheClient<T> {
} else {
if (encryption) {
final encryptionKey = await _getEncryptionKey();

if (encryptionKey == null) {
throw Exception(
'$runtimeType: Encryption enabled but key is null',
);
}

return Hive.openBox<T>(
tableName,
encryptionCipher:
encryptionKey != null ? HiveAesCipher(encryptionKey) : null,
encryptionCipher: HiveAesCipher(encryptionKey),
);
} else {
return Hive.openBox<T>(tableName);
Expand Down Expand Up @@ -337,5 +362,9 @@ abstract class HiveCacheClient<T> {
await Hive.box<T>(tableName).close();
}
}

if (PlatformInfo.isAndroid) {
AppSecurityManager.instance.clearKey();
}
}
}
16 changes: 16 additions & 0 deletions lib/features/caching/config/secure_storage_factory.dart
Original file line number Diff line number Diff line change
@@ -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,
),
);
}
}
5 changes: 5 additions & 0 deletions lib/features/caching/config/secure_storage_keys.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class SecureStorageKeys {
const SecureStorageKeys._();

static const String hiveEncryptionKey = 'hive_encryption_key';
}
196 changes: 196 additions & 0 deletions lib/features/caching/manager/app_security_manager.dart
Original file line number Diff line number Diff line change
@@ -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<void> 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<Uint8List?> 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<String?> _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<void> _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<void> _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<Uint8List> _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<bool> _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<void> _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<void> _wipeHiveSafe() async {
try {
logWarning('AppSecurityManager::_wipeHiveSafe: Deleting Hive data');
await Hive.deleteFromDisk();
} catch (e) {
logWarning('AppSecurityManager::_wipeHiveSafe: Delete failed, Exception $e');
rethrow;
}
}

Future<void> _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;
}
}
}
11 changes: 2 additions & 9 deletions lib/main/bindings/core/core_bindings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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());
}
Expand Down
10 changes: 9 additions & 1 deletion lib/main/main_entry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -20,6 +21,10 @@ Future<void> runTmail() async {
Future<void> runTmailPreload() async {
ThemeUtils.setSystemLightUIStyle();

if (PlatformInfo.isAndroid) {
await AppSecurityManager.instance.init();
}

await Future.wait([
MainBindings().dependencies(),
HiveCacheConfig.instance.setUp(),
Expand All @@ -29,7 +34,10 @@ Future<void> runTmailPreload() async {

await Get.find<Executor>().warmUp(log: BuildUtils.isDebugMode);
await CozyIntegration.integrateCozy();
await HiveCacheConfig.instance.initializeEncryptionKey();

if (!PlatformInfo.isAndroid) {
await HiveCacheConfig.instance.initializeEncryptionKey();
}

if (PlatformInfo.isWeb) {
setPathUrlStrategy();
Expand Down
Loading