diff --git a/app/lib/app_init.dart b/app/lib/app_init.dart index 04ec066c..7786c35d 100644 --- a/app/lib/app_init.dart +++ b/app/lib/app_init.dart @@ -21,6 +21,7 @@ import 'package:nc_photos/entity/local_file/data_source.dart'; import 'package:nc_photos/entity/nc_album/data_source.dart'; import 'package:nc_photos/entity/nc_album/repo.dart'; import 'package:nc_photos/entity/pref.dart'; +import 'package:nc_photos/entity/pref/provider/secure_storage.dart'; import 'package:nc_photos/entity/pref/provider/shared_preferences.dart'; import 'package:nc_photos/entity/pref_util.dart' as pref_util; import 'package:nc_photos/entity/recognize_face/data_source.dart'; @@ -137,6 +138,7 @@ void _initSelfSignedCertManager() { Future _initDiContainer(InitIsolateType isolateType) async { final c = DiContainer.late(); c.pref = Pref(); + c.securePref = await _createSecurePref(); c.npDb = await _createDb(isolateType); c.albumRepo = AlbumRepo(AlbumCachedDataSource(c)); @@ -204,5 +206,11 @@ Future _createDb(InitIsolateType isolateType) async { return npDb; } +Future _createSecurePref() async { + final provider = PrefSecureStorageProvider(); + await provider.init(); + return Pref.scoped(provider); +} + final _log = Logger("app_init"); var _hasInitedInThisIsolate = false; diff --git a/app/lib/controller/pref_controller.dart b/app/lib/controller/pref_controller.dart index bfded784..09c3306e 100644 --- a/app/lib/controller/pref_controller.dart +++ b/app/lib/controller/pref_controller.dart @@ -6,14 +6,16 @@ import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/pref.dart'; import 'package:nc_photos/language_util.dart'; import 'package:nc_photos/object_extension.dart'; +import 'package:nc_photos/protected_page_handler.dart'; import 'package:nc_photos/size.dart'; import 'package:np_codegen/np_codegen.dart'; +import 'package:np_common/object_util.dart'; import 'package:np_gps_map/np_gps_map.dart'; +import 'package:np_string/np_string.dart'; import 'package:rxdart/rxdart.dart'; part 'pref_controller.g.dart'; -@npLog @npSubjectAccessor class PrefController { PrefController(this._c); @@ -145,20 +147,13 @@ class PrefController { required BehaviorSubject controller, required Future Function(Pref pref, T value) setter, required T value, - }) async { - final backup = controller.value; - controller.add(value); - try { - if (!await setter(_c.pref, value)) { - throw StateError("Unknown error"); - } - } catch (e, stackTrace) { - _log.severe("[_set] Failed setting preference", e, stackTrace); - controller - ..addError(e, stackTrace) - ..add(backup); - } - } + }) => + _doSet( + pref: _c.pref, + controller: controller, + setter: setter, + value: value, + ); Future _setOrRemove({ required BehaviorSubject controller, @@ -166,26 +161,15 @@ class PrefController { required Future Function(Pref pref) remover, required T? value, T? defaultValue, - }) async { - final backup = controller.value; - controller.add(value ?? defaultValue); - try { - if (value == null) { - if (!await remover(_c.pref)) { - throw StateError("Unknown error"); - } - } else { - if (!await setter(_c.pref, value)) { - throw StateError("Unknown error"); - } - } - } catch (e, stackTrace) { - _log.severe("[_set] Failed setting preference", e, stackTrace); - controller - ..addError(e, stackTrace) - ..add(backup); - } - } + }) => + _doSetOrRemove( + pref: _c.pref, + controller: controller, + setter: setter, + remover: remover, + value: value, + defaultValue: defaultValue, + ); static AppLanguage _langIdToAppLanguage(int langId) { try { @@ -254,3 +238,92 @@ class PrefController { late final _secondarySeedColorController = BehaviorSubject.seeded( _c.pref.getSecondarySeedColor()?.run(Color.new)); } + +class SecurePrefController { + SecurePrefController(this._c); + + // ignore: unused_element + Future _set({ + required BehaviorSubject controller, + required Future Function(Pref pref, T value) setter, + required T value, + }) => + _doSet( + pref: _c.securePref, + controller: controller, + setter: setter, + value: value, + ); + + // ignore: unused_element + Future _setOrRemove({ + required BehaviorSubject controller, + required Future Function(Pref pref, T value) setter, + required Future Function(Pref pref) remover, + required T? value, + T? defaultValue, + }) => + _doSetOrRemove( + pref: _c.securePref, + controller: controller, + setter: setter, + remover: remover, + value: value, + defaultValue: defaultValue, + ); + + final DiContainer _c; +} + +Future _doSet({ + required Pref pref, + required BehaviorSubject controller, + required Future Function(Pref pref, T value) setter, + required T value, +}) async { + final backup = controller.value; + controller.add(value); + try { + if (!await setter(pref, value)) { + throw StateError("Unknown error"); + } + } catch (e, stackTrace) { + _$__NpLog.log.severe("[_doSet] Failed setting preference", e, stackTrace); + controller + ..addError(e, stackTrace) + ..add(backup); + } +} + +Future _doSetOrRemove({ + required Pref pref, + required BehaviorSubject controller, + required Future Function(Pref pref, T value) setter, + required Future Function(Pref pref) remover, + required T? value, + T? defaultValue, +}) async { + final backup = controller.value; + controller.add(value ?? defaultValue); + try { + if (value == null) { + if (!await remover(pref)) { + throw StateError("Unknown error"); + } + } else { + if (!await setter(pref, value)) { + throw StateError("Unknown error"); + } + } + } catch (e, stackTrace) { + _$__NpLog.log + .severe("[_doSetOrRemove] Failed setting preference", e, stackTrace); + controller + ..addError(e, stackTrace) + ..add(backup); + } +} + +@npLog +// ignore: camel_case_types +class __ {} diff --git a/app/lib/controller/pref_controller.g.dart b/app/lib/controller/pref_controller.g.dart index 7d807f7d..14990353 100644 --- a/app/lib/controller/pref_controller.g.dart +++ b/app/lib/controller/pref_controller.g.dart @@ -6,11 +6,11 @@ part of 'pref_controller.dart'; // NpLogGenerator // ************************************************************************** -extension _$PrefControllerNpLog on PrefController { +extension _$__NpLog on __ { // ignore: unused_element Logger get _log => log; - static final log = Logger("controller.pref_controller.PrefController"); + static final log = Logger("controller.pref_controller.__"); } // ************************************************************************** diff --git a/app/lib/di_container.dart b/app/lib/di_container.dart index 8e517167..26f1a119 100644 --- a/app/lib/di_container.dart +++ b/app/lib/di_container.dart @@ -51,6 +51,7 @@ enum DiType { pref, touchManager, npDb, + securePref, } class DiContainer { @@ -88,6 +89,7 @@ class DiContainer { Pref? pref, TouchManager? touchManager, NpDb? npDb, + Pref? securePref, }) : _albumRepo = albumRepo, _albumRepoRemote = albumRepoRemote, _albumRepoLocal = albumRepoLocal, @@ -120,7 +122,8 @@ class DiContainer { _recognizeFaceRepoLocal = recognizeFaceRepoLocal, _pref = pref, _touchManager = touchManager, - _npDb = npDb; + _npDb = npDb, + _securePref = securePref; DiContainer.late(); @@ -192,6 +195,8 @@ class DiContainer { return contianer._touchManager != null; case DiType.npDb: return contianer._npDb != null; + case DiType.securePref: + return contianer._securePref != null; } } @@ -213,6 +218,7 @@ class DiContainer { OrNull? pref, OrNull? touchManager, OrNull? npDb, + OrNull? securePref, }) { return DiContainer( albumRepo: albumRepo == null ? _albumRepo : albumRepo.obj, @@ -237,6 +243,7 @@ class DiContainer { pref: pref == null ? _pref : pref.obj, touchManager: touchManager == null ? _touchManager : touchManager.obj, npDb: npDb == null ? _npDb : npDb.obj, + securePref: securePref == null ? _securePref : securePref.obj, ); } @@ -277,6 +284,7 @@ class DiContainer { Pref get pref => _pref!; TouchManager get touchManager => _touchManager!; NpDb get npDb => _npDb!; + Pref get securePref => _securePref!; set albumRepo(AlbumRepo v) { assert(_albumRepo == null); @@ -443,6 +451,11 @@ class DiContainer { _npDb = v; } + set securePref(Pref v) { + assert(_securePref == null); + _securePref = v; + } + AlbumRepo? _albumRepo; AlbumRepo? _albumRepoRemote; // Explicitly request a AlbumRepo backed by local source @@ -480,6 +493,7 @@ class DiContainer { Pref? _pref; TouchManager? _touchManager; NpDb? _npDb; + Pref? _securePref; } extension DiContainerExtension on DiContainer { diff --git a/app/lib/entity/pref.dart b/app/lib/entity/pref.dart index 5aba4c5b..7117f9cb 100644 --- a/app/lib/entity/pref.dart +++ b/app/lib/entity/pref.dart @@ -6,7 +6,6 @@ import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/entity/pref/provider/memory.dart'; import 'package:np_codegen/np_codegen.dart'; -import 'package:np_common/type.dart'; part 'pref.g.dart'; part 'pref/extension.dart'; @@ -57,8 +56,6 @@ class AccountPref { } } - Future toJson() => provider.toJson(); - Future _set(AccountPrefKey key, T value, Future Function(AccountPrefKey key, T value) setFn) => setFn(key, value); @@ -241,6 +238,4 @@ abstract class PrefProvider { Future remove(PrefKeyInterface key); Future clear(); - - Future toJson(); } diff --git a/app/lib/entity/pref/provider/memory.dart b/app/lib/entity/pref/provider/memory.dart index dab6687d..aff1fad7 100644 --- a/app/lib/entity/pref/provider/memory.dart +++ b/app/lib/entity/pref/provider/memory.dart @@ -42,8 +42,6 @@ class PrefMemoryProvider extends PrefProvider { _data.clear(); return true; } - @override - Future toJson() async => Map.of(_data); T? _get(PrefKeyInterface key) => _data[key.toStringKey()]; diff --git a/app/lib/entity/pref/provider/secure_storage.dart b/app/lib/entity/pref/provider/secure_storage.dart new file mode 100644 index 00000000..6ae48eae --- /dev/null +++ b/app/lib/entity/pref/provider/secure_storage.dart @@ -0,0 +1,100 @@ +import 'dart:convert'; + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:logging/logging.dart'; +import 'package:nc_photos/entity/pref.dart'; +import 'package:np_codegen/np_codegen.dart'; +import 'package:np_common/object_util.dart'; + +part 'secure_storage.g.dart'; + +@npLog +class PrefSecureStorageProvider implements PrefProvider { + Future init() async { + _storage = const FlutterSecureStorage( + aOptions: AndroidOptions( + encryptedSharedPreferences: true, + preferencesKeyPrefix: "com.nkming.nc_photos", + sharedPreferencesName: "secure_pref", + ), + iOptions: IOSOptions( + accountName: "com.nkming.nc_photos", + ), + ); + _rawData = await _storage.readAll(); + } + + @override + bool? getBool(PrefKeyInterface key) { + final value = _rawData[key.toStringKey()]; + return value?.let((e) => bool.tryParse(e, caseSensitive: false)); + } + + @override + Future setBool(PrefKeyInterface key, bool value) => + setString(key, value.toString()); + + @override + int? getInt(PrefKeyInterface key) { + final value = _rawData[key.toStringKey()]; + return value?.let(int.tryParse); + } + + @override + Future setInt(PrefKeyInterface key, int value) => + setString(key, value.toString()); + + @override + String? getString(PrefKeyInterface key) { + return _rawData[key.toStringKey()]; + } + + @override + Future setString(PrefKeyInterface key, String value) async { + try { + await _storage.write(key: key.toStringKey(), value: value); + _rawData[key.toStringKey()] = value; + return true; + } catch (e, stackTrace) { + _log.severe("[setString] Failed while write", e, stackTrace); + return false; + } + } + + @override + List? getStringList(PrefKeyInterface key) { + final value = _rawData[key.toStringKey()]; + return (value?.let(jsonDecode) as List).cast(); + } + + @override + Future setStringList(PrefKeyInterface key, List value) => + setString(key, jsonEncode(value)); + + @override + Future remove(PrefKeyInterface key) async { + try { + await _storage.delete(key: key.toStringKey()); + _rawData.remove(key.toStringKey()); + return true; + } catch (e, stackTrace) { + _log.severe("[remove] Failed while write", e, stackTrace); + return false; + } + } + + @override + Future clear() async { + try { + await _storage.deleteAll(); + _rawData.clear(); + return true; + } catch (e, stackTrace) { + _log.severe("[clear] Failed while write", e, stackTrace); + return false; + } + } + + late FlutterSecureStorage _storage; + late Map _rawData; +} diff --git a/app/lib/entity/pref/provider/secure_storage.g.dart b/app/lib/entity/pref/provider/secure_storage.g.dart new file mode 100644 index 00000000..31b1bcd9 --- /dev/null +++ b/app/lib/entity/pref/provider/secure_storage.g.dart @@ -0,0 +1,15 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'secure_storage.dart'; + +// ************************************************************************** +// NpLogGenerator +// ************************************************************************** + +extension _$PrefSecureStorageProviderNpLog on PrefSecureStorageProvider { + // ignore: unused_element + Logger get _log => log; + + static final log = + Logger("entity.pref.provider.secure_storage.PrefSecureStorageProvider"); +} diff --git a/app/lib/entity/pref/provider/shared_preferences.dart b/app/lib/entity/pref/provider/shared_preferences.dart index 1a439715..eea95058 100644 --- a/app/lib/entity/pref/provider/shared_preferences.dart +++ b/app/lib/entity/pref/provider/shared_preferences.dart @@ -1,9 +1,7 @@ import 'package:nc_photos/entity/pref.dart'; import 'package:nc_photos/use_case/compat/v34.dart'; -import 'package:np_common/type.dart'; import 'package:np_universal_storage/np_universal_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; /// [Pref] stored with [SharedPreferences] lib class PrefSharedPreferencesProvider extends PrefProvider { @@ -55,8 +53,5 @@ class PrefSharedPreferencesProvider extends PrefProvider { @override Future clear() => _pref.clear(); - @override - Future toJson() => SharedPreferencesStorePlatform.instance.getAll(); - late SharedPreferences _pref; } diff --git a/app/lib/entity/pref/provider/universal_storage.dart b/app/lib/entity/pref/provider/universal_storage.dart index 8e57966b..7962deb2 100644 --- a/app/lib/entity/pref/provider/universal_storage.dart +++ b/app/lib/entity/pref/provider/universal_storage.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:nc_photos/entity/pref.dart'; -import 'package:np_common/type.dart'; import 'package:np_universal_storage/np_universal_storage.dart'; /// [Pref] backed by [UniversalStorage] @@ -52,9 +51,6 @@ class PrefUniversalStorageProvider extends PrefProvider { return true; } - @override - Future toJson() async => Map.of(_data); - T? _get(PrefKeyInterface key) => _data[key.toStringKey()]; Future _set(PrefKeyInterface key, T value) async { diff --git a/app/lib/widget/my_app.dart b/app/lib/widget/my_app.dart index ea140c38..e370b47f 100644 --- a/app/lib/widget/my_app.dart +++ b/app/lib/widget/my_app.dart @@ -72,6 +72,9 @@ class MyApp extends StatelessWidget { RepositoryProvider( create: (_) => PrefController(_c), ), + RepositoryProvider( + create: (_) => SecurePrefController(_c), + ), RepositoryProvider( create: (context) => AccountController( prefController: context.read(), diff --git a/app/pubspec.lock b/app/pubspec.lock index 1093e1c3..25f75ff8 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -553,6 +553,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.15" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "165164745e6afb5c0e3e3fcc72a012fb9e58496fb26ffb92cf22e16a821e85d0" + url: "https://pub.dev" + source: hosted + version: "9.2.2" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "4d91bfc23047422cbcd73ac684bc169859ee766482517c22172c86596bf1464b" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" flutter_staggered_grid_view: dependency: "direct main" description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 86336d32..a1be6b4a 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -69,6 +69,7 @@ dependencies: flutter_cache_manager: any flutter_colorpicker: ^1.0.3 flutter_isolate: ^2.0.4 + flutter_secure_storage: ^9.2.2 flutter_staggered_grid_view: git: url: https://gitlab.com/nc-photos/flutter_staggered_grid_view