From 8152829c895fb66e9571627d405f0dedfc6bc1d6 Mon Sep 17 00:00:00 2001 From: lollipopkit Date: Sat, 16 Sep 2023 17:26:40 +0800 Subject: [PATCH] #165 new: `bio auth` --- .dart_tool/flutter_gen/gen_l10n/l10n.dart | 12 ++++ .dart_tool/flutter_gen/gen_l10n/l10n_de.dart | 6 ++ .dart_tool/flutter_gen/gen_l10n/l10n_en.dart | 6 ++ .dart_tool/flutter_gen/gen_l10n/l10n_id.dart | 6 ++ .dart_tool/flutter_gen/gen_l10n/l10n_zh.dart | 12 ++++ android/app/src/main/AndroidManifest.xml | 1 + .../kotlin/tech/lolli/toolbox/MainActivity.kt | 4 +- ios/Podfile.lock | 6 ++ ios/Runner/Info-Debug.plist | 2 + ios/Runner/Info-Profile.plist | 2 + ios/Runner/Info-Release.plist | 2 + lib/core/utils/platform/auth.dart | 63 +++++++++++++++++++ lib/data/store/setting.dart | 7 +++ lib/l10n/app_de.arb | 2 + lib/l10n/app_en.arb | 2 + lib/l10n/app_id.arb | 2 + lib/l10n/app_zh.arb | 2 + lib/l10n/app_zh_tw.arb | 2 + lib/view/page/home.dart | 33 ++++++++++ lib/view/page/setting/entry.dart | 47 +++++++++++++- lib/view/widget/future_widget.dart | 2 +- pubspec.lock | 40 ++++++++++++ pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 25 files changed, 260 insertions(+), 6 deletions(-) create mode 100644 lib/core/utils/platform/auth.dart diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n.dart b/.dart_tool/flutter_gen/gen_l10n/l10n.dart index e3bfddeb..4d29426f 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n.dart @@ -164,6 +164,12 @@ abstract class S { /// **'Attention'** String get attention; + /// No description provided for @authRequired. + /// + /// In en, this message translates to: + /// **'Auth required'** + String get authRequired; + /// No description provided for @auto. /// /// In en, this message translates to: @@ -218,6 +224,12 @@ abstract class S { /// **'Run in backgroud'** String get bgRun; + /// No description provided for @bioAuth. + /// + /// In en, this message translates to: + /// **'Biometric auth'** + String get bioAuth; + /// No description provided for @canPullRefresh. /// /// In en, this message translates to: diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart index 9b2076f3..e3c1007f 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart @@ -37,6 +37,9 @@ class SDe extends S { @override String get attention => 'Achtung'; + @override + String get authRequired => 'Autorisierung erforderlich'; + @override String get auto => 'System folgen'; @@ -64,6 +67,9 @@ class SDe extends S { @override String get bgRun => 'Hintergrundaktualisierung'; + @override + String get bioAuth => 'Biozertifizierung'; + @override String get canPullRefresh => 'Danach: herunterziehen zum Aktualisieren'; diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart index 2437f210..e07dd5c1 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart @@ -37,6 +37,9 @@ class SEn extends S { @override String get attention => 'Attention'; + @override + String get authRequired => 'Auth required'; + @override String get auto => 'Auto'; @@ -64,6 +67,9 @@ class SEn extends S { @override String get bgRun => 'Run in backgroud'; + @override + String get bioAuth => 'Biometric auth'; + @override String get canPullRefresh => 'You can pull to refresh.'; diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_id.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_id.dart index 15329c1e..0d359e28 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n_id.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n_id.dart @@ -37,6 +37,9 @@ class SId extends S { @override String get attention => 'Perhatian'; + @override + String get authRequired => 'Auth diperlukan'; + @override String get auto => 'Auto'; @@ -64,6 +67,9 @@ class SId extends S { @override String get bgRun => 'Jalankan di Backgroud'; + @override + String get bioAuth => 'Biosertifikasi'; + @override String get canPullRefresh => 'Anda dapat menarik untuk menyegarkan.'; diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart index 1c0fd550..c5b915f0 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart @@ -37,6 +37,9 @@ class SZh extends S { @override String get attention => '注意'; + @override + String get authRequired => '需要认证'; + @override String get auto => '自动'; @@ -64,6 +67,9 @@ class SZh extends S { @override String get bgRun => '后台运行'; + @override + String get bioAuth => '生物认证'; + @override String get canPullRefresh => '可以下拉刷新'; @@ -772,6 +778,9 @@ class SZhTw extends SZh { @override String get attention => '注意'; + @override + String get authRequired => '需要認證'; + @override String get auto => '自動'; @@ -799,6 +808,9 @@ class SZhTw extends SZh { @override String get bgRun => '背景運行'; + @override + String get bioAuth => '生物認證'; + @override String get canPullRefresh => '可以下拉更新'; diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a034f105..d23f7a33 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ + UIViewControllerBasedStatusBarAppearance + NSFaceIDUsageDescription + Required for auth diff --git a/ios/Runner/Info-Profile.plist b/ios/Runner/Info-Profile.plist index 0de86afe..c39cec26 100644 --- a/ios/Runner/Info-Profile.plist +++ b/ios/Runner/Info-Profile.plist @@ -64,5 +64,7 @@ NSLocalNetworkUsageDescription ServerBox needs to access your local network to discover and connect to your server. + NSFaceIDUsageDescription + Required for auth diff --git a/ios/Runner/Info-Release.plist b/ios/Runner/Info-Release.plist index 4e13b6d1..cba4fe0c 100644 --- a/ios/Runner/Info-Release.plist +++ b/ios/Runner/Info-Release.plist @@ -58,5 +58,7 @@ UIViewControllerBasedStatusBarAppearance + NSFaceIDUsageDescription + Required for auth diff --git a/lib/core/utils/platform/auth.dart b/lib/core/utils/platform/auth.dart new file mode 100644 index 00000000..22a05a1d --- /dev/null +++ b/lib/core/utils/platform/auth.dart @@ -0,0 +1,63 @@ +import 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:local_auth/local_auth.dart'; +import 'package:toolbox/core/utils/platform/base.dart'; +import 'package:local_auth/error_codes.dart' as errs; + +class BioAuth { + const BioAuth._(); + + static final _auth = LocalAuthentication(); + + static bool get isPlatformSupported => isAndroid || isIOS || isWindows; + + static Future get isAvail async { + if (!isPlatformSupported) return false; + final canCheckBiometrics = await _auth.canCheckBiometrics; + if (!canCheckBiometrics) { + return false; + } + final biometrics = await _auth.getAvailableBiometrics(); + if (biometrics.isEmpty) return false; + return biometrics.contains(BiometricType.fingerprint) || + biometrics.contains(BiometricType.face); + } + + static Future auth([String? msg]) async { + if (!await isAvail) return AuthResult.notAvail; + try { + final reuslt = await _auth.authenticate( + localizedReason: msg ?? 'Auth required', + options: const AuthenticationOptions( + stickyAuth: true, + sensitiveTransaction: true, + biometricOnly: true, + ), + ); + if (reuslt) { + return AuthResult.success; + } + return AuthResult.fail; + } on PlatformException catch (e) { + switch (e.code) { + case errs.notEnrolled: + return AuthResult.notAvail; + case errs.lockedOut: + case errs.permanentlyLockedOut: + exit(0); + } + return AuthResult.cancel; + } + } +} + +enum AuthResult { + success, + // Not match + fail, + // User cancel + cancel, + // Device doesn't support biometrics + notAvail, +} diff --git a/lib/data/store/setting.dart b/lib/data/store/setting.dart index db289d00..14da1090 100644 --- a/lib/data/store/setting.dart +++ b/lib/data/store/setting.dart @@ -205,6 +205,13 @@ class SettingStore extends PersistentStore { false, ); + /// Only valid on iOS / Android + late final useBioAuth = StoreProperty( + box, + 'useBioAuth', + false, + ); + // Never show these settings for users // Guide for these settings: // - key should start with `_` and be shorter as possible diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 2480187a..c7cbe3b7 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -11,6 +11,7 @@ "alreadyLastDir": "Bereits im letzten Verzeichnis.", "alterUrl": "Url ändern", "attention": "Achtung", + "authRequired": "Autorisierung erforderlich", "auto": "System folgen", "autoCheckUpdate": "Aktualisierung automatisch prüfen", "autoConnect": "Automatisch verbinden", @@ -20,6 +21,7 @@ "backupTip": "Das Backup wird nur einfach verschlüsselt.\nBitte bewahre die Datei sicher auf.", "backupVersionNotMatch": "Die Backup-Version stimmt nicht überein.", "bgRun": "Hintergrundaktualisierung", + "bioAuth": "Biozertifizierung", "canPullRefresh": "Danach: herunterziehen zum Aktualisieren", "cancel": "Abbrechen", "choose": "Auswählen", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 631a71ec..4409bf1f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -11,6 +11,7 @@ "alreadyLastDir": "Already in last directory.", "alterUrl": "Alter url", "attention": "Attention", + "authRequired": "Auth required", "auto": "Auto", "autoCheckUpdate": "Auto check update", "autoConnect": "Auto connect", @@ -20,6 +21,7 @@ "backupTip": "The exported data is simply encrypted. \nPlease keep it safe.", "backupVersionNotMatch": "Backup version is not match.", "bgRun": "Run in backgroud", + "bioAuth": "Biometric auth", "canPullRefresh": "You can pull to refresh.", "cancel": "Cancel", "choose": "Choose", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 6b72da60..ef134fd7 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -11,6 +11,7 @@ "alreadyLastDir": "Sudah di direktori terakhir.", "alterUrl": "Alter url", "attention": "Perhatian", + "authRequired": "Auth diperlukan", "auto": "Auto", "autoCheckUpdate": "Periksa pembaruan otomatis", "autoConnect": "Hubungkan otomatis", @@ -20,6 +21,7 @@ "backupTip": "Data yang diekspor hanya dienkripsi.\nTolong jaga keamanannya.", "backupVersionNotMatch": "Versi cadangan tidak cocok.", "bgRun": "Jalankan di Backgroud", + "bioAuth": "Biosertifikasi", "canPullRefresh": "Anda dapat menarik untuk menyegarkan.", "cancel": "Membatalkan", "choose": "Memilih", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 02c24f74..11e654ae 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -11,6 +11,7 @@ "alreadyLastDir": "已经是最上层目录了", "alterUrl": "备选链接", "attention": "注意", + "authRequired": "需要认证", "auto": "自动", "autoCheckUpdate": "自动检查更新", "autoConnect": "自动连接", @@ -20,6 +21,7 @@ "backupTip": "导出的数据仅进行了简单加密,请妥善保管。", "backupVersionNotMatch": "备份版本不匹配,无法恢复", "bgRun": "后台运行", + "bioAuth": "生物认证", "canPullRefresh": "可以下拉刷新", "cancel": "取消", "choose": "选择", diff --git a/lib/l10n/app_zh_tw.arb b/lib/l10n/app_zh_tw.arb index e98681d0..8d83ad2a 100644 --- a/lib/l10n/app_zh_tw.arb +++ b/lib/l10n/app_zh_tw.arb @@ -11,6 +11,7 @@ "alreadyLastDir": "已經是最上層目錄了", "alterUrl": "備選鏈接", "attention": "注意", + "authRequired": "需要認證", "auto": "自動", "autoCheckUpdate": "自動檢查更新", "autoConnect": "自動連接", @@ -20,6 +21,7 @@ "backupTip": "導出的數據僅進行了簡單加密,請妥善保管。", "backupVersionNotMatch": "備份版本不匹配,無法還原", "bgRun": "背景運行", + "bioAuth": "生物認證", "canPullRefresh": "可以下拉更新", "cancel": "取消", "choose": "選擇", diff --git a/lib/view/page/home.dart b/lib/view/page/home.dart index 723662d1..c419c792 100644 --- a/lib/view/page/home.dart +++ b/lib/view/page/home.dart @@ -7,6 +7,7 @@ import 'package:get_it/get_it.dart'; import 'package:toolbox/core/channel/bg_run.dart'; import 'package:toolbox/core/channel/home_widget.dart'; import 'package:toolbox/core/extension/context/dialog.dart'; +import 'package:toolbox/core/utils/platform/auth.dart'; import 'package:toolbox/core/utils/platform/base.dart'; import 'package:toolbox/data/res/github_id.dart'; import 'package:toolbox/data/res/logger.dart'; @@ -46,6 +47,7 @@ class _HomePageState extends State late S _s; bool _switchingPage = false; + bool _isAuthing = false; @override void initState() { @@ -81,6 +83,7 @@ class _HomePageState extends State switch (state) { case AppLifecycleState.resumed: + _auth(); if (!Providers.server.isAutoRefreshOn) { Providers.server.startAutoRefresh(); } @@ -338,6 +341,8 @@ class _HomePageState extends State @override Future afterFirstLayout(BuildContext context) async { + // Auth required for first launch + _auth(); if (Stores.setting.autoCheckAppUpdate.fetch()) { doUpdate(context); } @@ -385,4 +390,32 @@ class _HomePageState extends State Loggers.app.warning('Update json settings failed', e, trace); } } + + void _auth() { + if (Stores.setting.useBioAuth.fetch()) { + if (!_isAuthing) { + _isAuthing = true; + BioAuth.auth(_s.authRequired).then( + (val) { + switch (val) { + case AuthResult.success: + // wait for animation + Future.delayed( + const Duration(seconds: 1), () => _isAuthing = false); + break; + case AuthResult.fail: + case AuthResult.cancel: + _isAuthing = false; + _auth(); + break; + case AuthResult.notAvail: + _isAuthing = false; + Stores.setting.useBioAuth.put(false); + break; + } + }, + ); + } + } + } } diff --git a/lib/view/page/setting/entry.dart b/lib/view/page/setting/entry.dart index d7b81f76..5d8637ab 100644 --- a/lib/view/page/setting/entry.dart +++ b/lib/view/page/setting/entry.dart @@ -12,6 +12,7 @@ import 'package:toolbox/core/extension/context/snackbar.dart'; import 'package:toolbox/core/extension/locale.dart'; import 'package:toolbox/core/extension/context/dialog.dart'; import 'package:toolbox/core/extension/stringx.dart'; +import 'package:toolbox/core/utils/platform/auth.dart'; import 'package:toolbox/core/utils/platform/base.dart'; import 'package:toolbox/data/res/provider.dart'; import 'package:toolbox/data/res/store.dart'; @@ -183,13 +184,16 @@ class _SettingPageState extends State { //_buildLaunchPage(), _buildCheckUpdate(), ]; + if (isAndroid) { + children.add(_buildBgRun()); + children.add(_buildAndroidWidgetSharedPreference()); + } if (isIOS) { children.add(_buildPushToken()); children.add(_buildAutoUpdateHomeWidget()); } - if (isAndroid) { - children.add(_buildBgRun()); - children.add(_buildAndroidWidgetSharedPreference()); + if (BioAuth.isPlatformSupported) { + children.add(_buildBioAuth()); } return Column( children: children.map((e) => RoundRectCard(e)).toList(), @@ -1096,4 +1100,41 @@ class _SettingPageState extends State { // trailing: StoreSwitch(prop: _setting.doubleColumnServersPage), // ); // } + + Widget _buildBioAuth() { + return FutureWidget( + future: BioAuth.isAvail, + loading: ListTile( + title: Text(_s.bioAuth), + subtitle: Text(_s.serverTabLoading, style: UIs.textGrey), + ), + error: (e, __) => ListTile( + title: Text(_s.bioAuth), + subtitle: Text('${_s.failed}: $e', style: UIs.textGrey), + ), + success: (can) { + return ListTile( + title: Text(_s.bioAuth), + trailing: can + ? StoreSwitch( + prop: Stores.setting.useBioAuth, + func: (val) async { + if (val) { + Stores.setting.useBioAuth.put(false); + return; + } + // Only auth when turn off (val == false) + final result = await BioAuth.auth(_s.authRequired); + // If failed, turn on again + if (result != AuthResult.success) { + Stores.setting.useBioAuth.put(true); + } + }, + ) + : Text(_s.error, style: UIs.textGrey), + ); + }, + noData: UIs.placeholder, + ); + } } diff --git a/lib/view/widget/future_widget.dart b/lib/view/widget/future_widget.dart index d3fe30c4..72d87f06 100644 --- a/lib/view/widget/future_widget.dart +++ b/lib/view/widget/future_widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; class FutureWidget extends StatelessWidget { - final Future future; + final Future future; final Widget loading; final Widget Function(Object? error, StackTrace? trace) error; final Widget Function(T data) success; diff --git a/pubspec.lock b/pubspec.lock index c5fe884e..b32c45bb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -518,6 +518,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + local_auth: + dependency: "direct main" + description: + name: local_auth + sha256: "7e6c63082e399b61e4af71266b012e767a5d4525dd6e9ba41e174fd42d76e115" + url: "https://pub.dev" + source: hosted + version: "2.1.7" + local_auth_android: + dependency: transitive + description: + name: local_auth_android + sha256: "9ad0b1ffa6f04f4d91e38c2d4c5046583e23f4cae8345776a994e8670df57fb1" + url: "https://pub.dev" + source: hosted + version: "1.0.34" + local_auth_ios: + dependency: transitive + description: + name: local_auth_ios + sha256: "26a8d1ad0b4ef6f861d29921be8383000fda952e323a5b6752cf82ca9cf9a7a9" + url: "https://pub.dev" + source: hosted + version: "1.1.4" + local_auth_platform_interface: + dependency: transitive + description: + name: local_auth_platform_interface + sha256: fc5bd537970a324260fda506cfb61b33ad7426f37a8ea5c461cf612161ebba54 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + local_auth_windows: + dependency: transitive + description: + name: local_auth_windows + sha256: "505ba3367ca781efb1c50d3132e44a2446bccc4163427bc203b9b4d8994d97ea" + url: "https://pub.dev" + source: hosted + version: "1.0.10" logging: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 1bee845b..609152d0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,6 +49,7 @@ dependencies: macos_window_utils: ^1.2.0 dynamic_color: ^1.6.6 icloud_storage: ^2.2.0 + local_auth: ^2.1.7 dev_dependencies: flutter_native_splash: ^2.1.6 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 1a40a844..7a68426d 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,12 +7,15 @@ #include "generated_plugin_registrant.h" #include +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { DynamicColorPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); + LocalAuthPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("LocalAuthPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 9feb0426..d8bef0fa 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST dynamic_color + local_auth_windows share_plus url_launcher_windows )