Quellcode durchsuchen

bio 功能 ok,UI 需完善

James vor 3 Wochen
Ursprung
Commit
8dc297fb3c

+ 7 - 5
lib/appfx.dart

@@ -1,12 +1,14 @@
-library appfx;
+library;
 
-export 'util/tools.dart';
-export 'util/fileUtils.dart';
-export 'util/logger.dart';
-export 'util/riverpodUtils.dart';
+export 'utils/tools.dart';
+export 'utils/fileUtils.dart';
+export 'utils/logger.dart';
+export 'utils/riverpodUtils.dart';
+export 'utils/biometricAuthUtils.dart';
 
 export 'store/objbxDbMgrBase.dart';
 export 'store/semDbMgrBase.dart';
 export 'store/sharedStore.dart';
+export 'store/secureStore.dart';
 
 export 'theme/appTheme.dart';

+ 54 - 0
lib/mathUtils/aesUtils.dart

@@ -0,0 +1,54 @@
+
+import 'dart:convert';
+import 'dart:typed_data';
+import 'package:encrypt/encrypt.dart';
+
+
+class CryptData {
+  final Uint8List _byte;
+
+  CryptData(Uint8List byte) : _byte = byte;
+  CryptData.fromUtf8(String base64data) : _byte = base64Decode(base64data);
+  CryptData.fromBase64(String base64data) : _byte = base64Decode(base64data);
+  CryptData.fromHex(String hexData) : _byte = decodeHexString(hexData);
+
+  Uint8List get byte => _byte;
+  String get base64 => base64Encode(_byte);
+}
+
+class EncryptedAesCbc {
+  final CryptData iv;
+  final CryptData encrypted;
+
+  EncryptedAesCbc({
+    required this.iv,
+    required this.encrypted,
+  });
+
+  EncryptedAesCbc.fromBase64({
+    required String base64iv,
+    required String base64encrypted,
+  }) : iv = CryptData.fromBase64(base64iv), encrypted = CryptData.fromBase64(base64encrypted);
+
+}
+
+EncryptedAesCbc encryptAesCbc(String plainText, CryptData password) {
+  final iv = IV.fromLength(16); // AES-CBC 必需的16字节IV
+  final key = Key(password.byte);
+  final encrypter = Encrypter(AES(key, mode: AESMode.cbc));
+
+  final encrypted = encrypter.encrypt(plainText, iv: iv);
+
+  return EncryptedAesCbc(iv: CryptData(iv.bytes), encrypted: CryptData(encrypted.bytes));
+}
+
+
+String decryptAesCbc(EncryptedAesCbc encrypted, CryptData password) {
+  final iv = IV(encrypted.iv.byte);
+  final key = Key(password.byte);
+  final encrypter = Encrypter(AES(key, mode: AESMode.cbc));
+
+  final decrypted = encrypter.decrypt(Encrypted(encrypted.encrypted.byte), iv: iv);
+
+  return decrypted;
+}

+ 185 - 0
lib/mathUtils/keyUtils.dart

@@ -0,0 +1,185 @@
+
+import 'dart:convert';
+
+import 'dart:math';
+import 'package:pointycastle/pointycastle.dart';
+import 'package:pointycastle/key_derivators/pbkdf2.dart';
+import 'package:pointycastle/digests/sha256.dart';
+import 'package:pointycastle/macs/hmac.dart';
+
+
+// 随机字符串
+String randomString(int len) {
+  if (len <= 0) {
+    len = 16;
+  }
+  // 安全字符集(去掉Base64中的特殊字符 + / =)
+  const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-';
+  final random = Random.secure();
+  final result = StringBuffer();
+
+  for (int i = 0; i < len; i++) {
+    final index = random.nextInt(charset.length);
+    result.write(charset[index]);
+  }
+
+  return result.toString();
+}
+
+String randomStringEx(int len, {
+  final bool includeLowercase = true,
+  final bool includeUppercase = true,
+  final bool includeDigits = true,
+  final bool includeSpecialChars = true,
+  final String? customCharset,
+}) {
+  if (len <= 0) {
+    len = 16;
+  }
+
+  String? charset;
+  if (customCharset != null) {
+    charset = customCharset;
+  } else {
+    final buffer = StringBuffer();
+    if (includeLowercase) buffer.write('abcdefghijklmnopqrstuvwxyz');
+    if (includeUppercase) buffer.write('ABCDEFGHIJKLMNOPQRSTUVWXYZ');
+    if (includeDigits) buffer.write('0123456789');
+    if (includeSpecialChars) buffer.write('_-');
+    charset = buffer.toString();
+  }
+  if (charset.isEmpty) {
+    throw StateError('empty charset');
+  }
+
+  final random = Random.secure();
+  final result = StringBuffer();
+
+  for (int i = 0; i < len; i++) {
+    final index = random.nextInt(charset.length);
+    result.write(charset[index]);
+  }
+
+  return result.toString();
+}
+
+/// password to PBKDF2 password
+String passwordWithPBKDF2({
+  required String password,
+  required String salt,
+  int iterations = 100000,
+  int keyLength = 32,
+}) {
+  final seed = utf8.encode(password);
+  final saltBytes = utf8.encode(salt);
+
+  // 1. 初始化 PBKDF2 派生器(HMAC-SHA256 作为伪随机函数)
+  final pbkdf2 = PBKDF2KeyDerivator(
+    HMac(SHA256Digest(), 64), // HMAC-SHA256,64字节块大小
+  );
+
+  // 2. 设置参数:盐值 + 迭代次数 + 密钥长度(AES-256 需要 32 字节)
+  final params = Pbkdf2Parameters(
+    saltBytes,
+    iterations, // 迭代次数(越高越安全,建议 10万+)
+    keyLength,     // 输出密钥长度(32字节 = 256位,适配 AES-256)
+  );
+  pbkdf2.init(params);
+
+  // 3. 派生密钥(返回 32 字节的 AES-256 密钥)
+  final keyBytes = pbkdf2.process(seed);
+  return base64Encode(keyBytes);
+}
+
+
+//
+// /// ---------- 使用 HMAC-SHA256 实现 PBKDF2 ----------
+// ///
+// /// - [password]: 用户密码(UTF-8 编码后的字节)
+// /// - [salt]: 盐(建议随机生成)
+// /// - [iterations]: 迭代次数(如 10000)
+// /// - [keyLength]: 派生密钥长度(字节,如 32 = 256 位)
+//
+// String generatePasswordWithPBKDF2({
+//   required String password,
+//   required String salt,
+//   int iterations = 10000,
+//   int keyLength = 32,
+// }) {
+//   final key = base64.encode(pbkdf2HmacSha256(
+//     password: utf8.encode(password),
+//     salt: utf8.encode(salt),
+//     iterations: iterations,
+//     keyLength: keyLength,
+//   ));
+//   return key;
+// }
+//
+// Uint8List pbkdf2HmacSha256({
+//   required Uint8List password,
+//   required Uint8List salt,
+//   int iterations = 100000,
+//   int keyLength = 32,
+// }) {
+//   if (iterations < 1) throw ArgumentError('Iterations must be >= 1');
+//   if (keyLength <= 0) throw ArgumentError('Key length must be > 0');
+//
+//   final List<int> derivedKey = [];
+//   final int hLen = 32; // SHA-256 输出长度(字节)
+//   final int l = (keyLength + hLen - 1) ~/ hLen; // 块数
+//
+//   for (int i = 1; i <= l; i++) {
+//     // T_i = F(Password, Salt, Iterations, i)
+//     final blockSalt = _concat(salt, _encodeInt(i));
+//     final Uint8List u = hmacSha256(password, blockSalt);
+//     Uint8List t = u;
+//
+//     for (int j = 1; j < iterations; j++) {
+//       final Uint8List uNext = hmacSha256(password, u);
+//       t = _xor(t, uNext);
+//       u.setAll(0, uNext); // 更新 u 为 uNext
+//     }
+//
+//     derivedKey.addAll(t);
+//   }
+//
+//   return Uint8List.fromList(derivedKey.sublist(0, keyLength));
+// }
+//
+// // HMAC-SHA256 辅助函数
+// Uint8List hmacSha256(Uint8List key, Uint8List data) {
+//   final hmac = Hmac(sha256, key);
+//   final digest = hmac.convert(data);
+//   return Uint8List.fromList(digest.bytes);
+// }
+//
+// // 将整数编码为大端 4 字节(符合 RFC 2898)
+// Uint8List _encodeInt(int i) {
+//   return Uint8List.fromList([
+//     (i >> 24) & 0xFF,
+//     (i >> 16) & 0xFF,
+//     (i >> 8) & 0xFF,
+//     i & 0xFF,
+//   ]);
+// }
+//
+// // 拼接两个 Uint8List
+// Uint8List _concat(Uint8List a, Uint8List b) {
+//   final result = Uint8List(a.length + b.length);
+//   result.setAll(0, a);
+//   result.setAll(a.length, b);
+//   return result;
+// }
+//
+// // 按字节异或两个等长 Uint8List
+// Uint8List _xor(Uint8List a, Uint8List b) {
+//   if (a.length != b.length) {
+//     throw ArgumentError('XOR requires equal-length inputs');
+//   }
+//   final result = Uint8List(a.length);
+//   for (int i = 0; i < a.length; i++) {
+//     result[i] = a[i] ^ b[i];
+//   }
+//   return result;
+// }
+

+ 4 - 0
lib/mathUtils/mathUtils.dart

@@ -0,0 +1,4 @@
+
+export 'keyUtils.dart';
+export 'aesUtils.dart';
+

+ 1 - 1
lib/network/httpServerWrap.dart

@@ -2,7 +2,7 @@
 import 'dart:async';
 import 'dart:io';
 import 'dart:convert';
-// import '../util/logger.dart';
+// import '../utils/logger.dart';
 
 
 enum HttpServerStatus {

+ 1 - 1
lib/network/websocketServerWrap.dart

@@ -1,7 +1,7 @@
 
 import 'dart:async';
 import 'dart:io';
-// import '../util/logger.dart';
+// import '../utils/logger.dart';
 
 
 enum WebsocketServerStatus {

+ 1 - 1
lib/store/secureStore.dart

@@ -71,7 +71,7 @@ class SecureStore {
     await storage.delete(key: key);
   }
   // 查
-  Future<bool> containsKey(String key) async {
+  Future<bool> containsKey(String key) {
     return storage.containsKey(key: key);
   }
 

+ 0 - 0
lib/util/base64Utils.dart → lib/utils/base64Utils.dart


+ 85 - 0
lib/utils/biometricAuthUtils.dart

@@ -0,0 +1,85 @@
+
+import 'package:flutter/services.dart';
+import 'package:appfx/utils/logger.dart';
+import 'package:local_auth/local_auth.dart';
+import 'package:flutter_secure_storage/flutter_secure_storage.dart';
+
+export 'package:local_auth/local_auth.dart' show BiometricType;
+
+
+class BiometricAuthUtils {
+
+  bool canCheckBiometrics = false;
+  List<BiometricType> availableBiometrics = [];
+
+  final LocalAuthentication auth = LocalAuthentication();
+  final FlutterSecureStorage storage = const FlutterSecureStorage();
+
+  Future<void> init() async {
+    try {
+      canCheckBiometrics = await checkBiometrics();
+      if (canCheckBiometrics) {
+        availableBiometrics = await getAvailableBiometrics();
+      }
+    } catch (error) {
+      Log.e('init error: $error');
+    }
+  }
+
+  bool get isSupportFaceId {
+    return availableBiometrics.any((type) => type == BiometricType.face);
+  }
+  bool get isSupportTouchId {
+    return availableBiometrics.any((type) => type == BiometricType.fingerprint);
+  }
+
+
+
+  // 检查设备是否支持生物识别
+  Future<bool> checkBiometrics() async {
+    try {
+      canCheckBiometrics = await auth.canCheckBiometrics;
+      return canCheckBiometrics;
+    } on PlatformException catch (e) {
+      Log.e('检查设备是否支持生物识别失败: $e');
+      canCheckBiometrics = false;
+      return false;
+    }
+  }
+
+  // 获取可用的生物识别类型
+  Future<List<BiometricType>> getAvailableBiometrics() async {
+    try {
+      availableBiometrics = await auth.getAvailableBiometrics();
+      return availableBiometrics;
+    } on PlatformException catch (e) {
+      Log.e('获取生物识别类型失败: $e');
+      availableBiometrics = [];
+      return availableBiometrics;
+    }
+  }
+
+  // 进行生物识别认证
+  Future<bool> authenticateWithBiometrics(String reason) async {
+    try {
+      final bool didAuthenticate = await auth.authenticate(
+        localizedReason: reason, // '请进行人脸识别以访问您的密码',
+        biometricOnly: true,
+        sensitiveTransaction: true,
+        persistAcrossBackgrounding: false,
+      );
+      return didAuthenticate;
+    } on PlatformException catch (e) {
+      Log.e('认证失败: $e');
+      return false;
+    }
+  }
+
+
+}
+
+
+
+
+
+

+ 57 - 0
lib/utils/deviceUuid.dart

@@ -0,0 +1,57 @@
+
+// device_info_plus 中 androidInfo.androidId 被移除,无法获取跨应用的设备唯一标识
+// 为了代码一致性,iOS 端也不使用 iosInfo.identifierForVendor
+// 统一使用 uuid.dart 来随机生成
+
+import 'package:shared_preferences/shared_preferences.dart';
+import 'package:uuid/uuid.dart';
+
+class DeviceUuid {
+  static const String _uuidKey = 'device_uuid';
+  static String? _cachedUuid;
+
+  // 获取或创建 UUID
+  static Future<String> getUuid() async {
+    if (_cachedUuid != null) {
+      return _cachedUuid!;
+    }
+
+    final prefs = await SharedPreferences.getInstance();
+    String? storedUuid = prefs.getString(_uuidKey);
+
+    if (storedUuid == null || storedUuid.isEmpty) {
+      storedUuid = _generateAndStoreUuid(prefs);
+    }
+
+    _cachedUuid = storedUuid;
+    return storedUuid;
+  }
+
+  // 生成并存储新的 UUID
+  static String _generateAndStoreUuid(SharedPreferences prefs) {
+    final newUuid = const Uuid().v4();
+    prefs.setString(_uuidKey, newUuid);
+    return newUuid;
+  }
+
+  // 重置 UUID(用户选择重置隐私时使用)
+  static Future<String> resetUuid() async {
+    final prefs = await SharedPreferences.getInstance();
+    _cachedUuid = null;
+    return _generateAndStoreUuid(prefs);
+  }
+
+  // 检查是否已有 UUID
+  static Future<bool> hasUuid() async {
+    final prefs = await SharedPreferences.getInstance();
+    return prefs.containsKey(_uuidKey);
+  }
+
+  // 清除 UUID(用户注销时)
+  static Future<void> clearUuid() async {
+    final prefs = await SharedPreferences.getInstance();
+    await prefs.remove(_uuidKey);
+    _cachedUuid = null;
+  }
+}
+

+ 0 - 0
lib/util/fileUtils.dart → lib/utils/fileUtils.dart


+ 0 - 0
lib/util/hexUtils.dart → lib/utils/hexUtils.dart


+ 4 - 1
lib/util/logger.dart → lib/utils/logger.dart

@@ -38,7 +38,10 @@ class Log {
   }
 
   // json
-  static void json(Map json) {
+  static void json(Map json, {String? title}) {
+    if (title != null) {
+      print(title);
+    }
     const encoder = JsonEncoder.withIndent('  '); // 2空格缩进
     final prettyString = encoder.convert(json);
     print(prettyString);

+ 0 - 0
lib/util/riverpodUtils.dart → lib/utils/riverpodUtils.dart


+ 1 - 3
lib/util/tools.dart → lib/utils/tools.dart

@@ -1,7 +1,5 @@
-import 'dart:convert';
-
-export 'hexUtils.dart';
 
+import 'dart:convert';
 
 
 // DateFormat('dd.MM.yy HH:mm:ss').format(dateCreated); // ??

+ 105 - 1
pubspec.lock

@@ -33,6 +33,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.7.0"
+  asn1lib:
+    dependency: transitive
+    description:
+      name: asn1lib
+      sha256: "9a8f69025044eb466b9b60ef3bc3ac99b4dc6c158ae9c56d25eeccf5bc56d024"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.6.5"
   async:
     dependency: transitive
     description:
@@ -153,6 +161,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.4.2"
+  clock:
+    dependency: transitive
+    description:
+      name: clock
+      sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.2"
   code_builder:
     dependency: transitive
     description:
@@ -225,6 +241,30 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.3.6"
+  device_info_plus:
+    dependency: "direct main"
+    description:
+      name: device_info_plus
+      sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074
+      url: "https://pub.dev"
+    source: hosted
+    version: "10.1.2"
+  device_info_plus_platform_interface:
+    dependency: transitive
+    description:
+      name: device_info_plus_platform_interface
+      sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.0.3"
+  encrypt:
+    dependency: "direct main"
+    description:
+      name: encrypt
+      sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2"
+      url: "https://pub.dev"
+    source: hosted
+    version: "5.0.3"
   ffi:
     dependency: transitive
     description:
@@ -326,6 +366,14 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.0"
+  flutter_plugin_android_lifecycle:
+    dependency: transitive
+    description:
+      name: flutter_plugin_android_lifecycle
+      sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.33"
   flutter_riverpod:
     dependency: "direct main"
     description:
@@ -451,6 +499,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "4.1.2"
+  intl:
+    dependency: transitive
+    description:
+      name: intl
+      sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.20.2"
   io:
     dependency: transitive
     description:
@@ -475,6 +531,46 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "4.9.0"
+  local_auth:
+    dependency: "direct main"
+    description:
+      name: local_auth
+      sha256: a4f1bf57f0236a4aeb5e8f0ec180e197f4b112a3456baa6c1e73b546630b0422
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.0"
+  local_auth_android:
+    dependency: transitive
+    description:
+      name: local_auth_android
+      sha256: "162b8e177fd9978c4620da2a8002a5c6bed4d20f0c6daf5137e72e9a8b767d2e"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.4"
+  local_auth_darwin:
+    dependency: transitive
+    description:
+      name: local_auth_darwin
+      sha256: "668ea65edaab17380956e9713f57e34f78ede505ca0cfd8d39db34e2f260bfee"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.1"
+  local_auth_platform_interface:
+    dependency: transitive
+    description:
+      name: local_auth_platform_interface
+      sha256: f98b8e388588583d3f781f6806e4f4c9f9e189d898d27f0c249b93a1973dd122
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.0"
+  local_auth_windows:
+    dependency: transitive
+    description:
+      name: local_auth_windows
+      sha256: be12c5b8ba5e64896983123655c5f67d2484ecfcc95e367952ad6e3bff94cb16
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.1"
   logger:
     dependency: "direct main"
     description:
@@ -889,7 +985,7 @@ packages:
     source: hosted
     version: "1.4.0"
   uuid:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: uuid
       sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
@@ -952,6 +1048,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "5.15.0"
+  win32_registry:
+    dependency: transitive
+    description:
+      name: win32_registry
+      sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.5"
   xdg_directories:
     dependency: transitive
     description:

+ 3 - 0
pubspec.yaml

@@ -27,6 +27,9 @@ dependencies:
   shared_preferences: ^2.2.2
   flutter_secure_storage: ^9.2.4
   pointycastle: ^3.9.1
+  local_auth: ^3.0.0
+  device_info_plus: ^10.1.2
+  uuid: ^4.5.2
 
 dev_dependencies:
   build_runner: ^2.4.8