From a063ce178e03012331b3028e3186e1ad32a00880 Mon Sep 17 00:00:00 2001 From: Cody Date: Mon, 9 Mar 2026 09:06:39 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E8=A7=A3=E5=AF=86=E6=80=A7=E8=83=BD?= =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=8C=E9=A2=84=E5=9F=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Doc/IM_App_架构设计.html | 10 +- apps/im_app/lib/app/di/app_providers.dart | 11 +- apps/im_app/lib/app/di/network_provider.dart | 20 +++- .../encryption_flutter_service.dart | 106 +++++++++++++++--- .../facade/cipher_guard_sdk_api.dart | 31 +++++ .../wiring/cipher_guard_sdk_api_impl.dart | 43 +++++++ 6 files changed, 199 insertions(+), 22 deletions(-) diff --git a/Doc/IM_App_架构设计.html b/Doc/IM_App_架构设计.html index 8d9dde1..12edacb 100644 --- a/Doc/IM_App_架构设计.html +++ b/Doc/IM_App_架构设计.html @@ -427,6 +427,14 @@ • 日志与监控系统 Part 7:总结 + + Part 8:UI 设计规范 + • 核心约定 + • 颜色体系 + • 字体体系 + • 组件 — Button + • 业务弹框 — Dialog + • 图标规范 @@ -6507,7 +6515,7 @@ class UploadFileRequest extends ApiRequestable<UploadResult> } -

模式 B:二进制上传到 S3 presigned URL(参考 LingoDot-Flutter)

+

模式 B:二进制上传到 S3 presigned URL

先向后端获取 presigned URL,再直接上传到 S3:

diff --git a/apps/im_app/lib/app/di/app_providers.dart b/apps/im_app/lib/app/di/app_providers.dart index c5e6f28..c76c412 100644 --- a/apps/im_app/lib/app/di/app_providers.dart +++ b/apps/im_app/lib/app/di/app_providers.dart @@ -23,11 +23,18 @@ class AuthNotifier extends ChangeNotifier { void login() { _isLoggedIn = true; + // TODO: 接入 cipher_guard_sdk 后,在此处完成 RSA 密钥注入: + // 1. 从安全存储(keychain / secure storage)读取公私钥对(只读一次) + // 2. cipherSdk.setActiveKeyPair(publicKey: pubPem, privateKey: privPem) + // 须在 notifyListeners() 之前完成,确保路由跳转后 onEncryptRequest 回调触发时密钥已就绪。 notifyListeners(); } void logout() { _isLoggedIn = false; + // TODO: 接入 cipher_guard_sdk 后,退出登录时清除内存密钥: + // cipherSdk.clearActiveKeyPair() + // cipherSdk.clearDerivedKeyCache() notifyListeners(); } } @@ -37,9 +44,7 @@ class AuthNotifier extends ChangeNotifier { /// 使用 [Provider] 持有 [AuthNotifier] 单例。 /// go_router 通过 [GoRouter.refreshListenable] 直接监听 [AuthNotifier](ChangeNotifier), /// Riverpod 侧不需要响应式更新(导航由 go_router 接管)。 -final authNotifierProvider = Provider( - (ref) => AuthNotifier(), -); +final authNotifierProvider = Provider((ref) => AuthNotifier()); // ── 主题 ────────────────────────────────────────────────────────────────────── diff --git a/apps/im_app/lib/app/di/network_provider.dart b/apps/im_app/lib/app/di/network_provider.dart index d02678e..be1fd35 100644 --- a/apps/im_app/lib/app/di/network_provider.dart +++ b/apps/im_app/lib/app/di/network_provider.dart @@ -99,8 +99,24 @@ final apiConfigProvider = Provider((ref) { tokenStream.add(newToken); }, onCheckNetworkAvailable: () async => networkMonitor.isConnected, - onEncryptRequest: null, // TODO: 接入 cipher_guard_sdk 后注入请求加密回调 - onDecryptResponse: null, // TODO: 接入 cipher_guard_sdk 后注入响应解密回调 + // TODO: 接入 cipher_guard_sdk 后注入请求加密回调。 + // 前提:AuthNotifier.login() 中已完成 cipherSdk.setActiveKeyPair(pub, priv)。 + // 示例: + // onEncryptRequest: (path, headers, body) async { + // final encryptedKey = await cipherSdk.encryptSessionKeyWithActiveKey( + // sessionKey: currentSessionKey, + // ); + // return EncryptedRequest(body: encryptedBody, headers: {'X-Key': encryptedKey}); + // }, + onEncryptRequest: null, + // TODO: 接入 cipher_guard_sdk 后注入响应解密回调。 + // 前提:与 onEncryptRequest 配套,服务端响应同样加密时启用。 + // 示例: + // onDecryptResponse: (data) async { + // final plaintext = await cipherSdk.decryptMessage(encryptedData: data as String, ...); + // return jsonDecode(plaintext) as Map; + // }, + onDecryptResponse: null, onBusinessError: null, // TODO: 接入业务错误统一处理(弹窗 / Toast / 跳转等) onTransformResponse: null, // TODO: 如后端响应格式非标准,在此归一化为 { code, data, message } diff --git a/packages/cipher_guard_sdk/lib/src/data/datasources/encryption_flutter_service.dart b/packages/cipher_guard_sdk/lib/src/data/datasources/encryption_flutter_service.dart index cd23a12..56ee26e 100644 --- a/packages/cipher_guard_sdk/lib/src/data/datasources/encryption_flutter_service.dart +++ b/packages/cipher_guard_sdk/lib/src/data/datasources/encryption_flutter_service.dart @@ -21,12 +21,12 @@ import 'package:pointycastle/random/fortuna_random.dart'; /// 密钥派生模式 /// /// 决定 [EncryptionFlutterService._deriveKeyForRound] 使用哪种算法。 -/// 默认 [md5](UU 兼容),可选 [pbkdf2](增强安全性)。 +/// 默认 [md5],可选 [pbkdf2](增强安全性)。 /// /// 解密旧数据时必须使用加密时相同的模式, /// 通过消息的 version 字段区分。 enum KdfMode { - /// MD5 简单哈希(UU 兼容默认模式) + /// MD5 简单哈希(默认模式) /// /// 适用于 session key 已是 32 字节强随机值的场景。 /// 性能好,每次调用 < 0.1ms。 @@ -48,14 +48,26 @@ enum KdfMode { /// /// - **RSA 密钥生成**:通过 [generateRsaKeyPairAsync] 在 Isolate 中运行, /// 避免阻塞主线程(1024-bit 约 150ms,2048-bit 约 300ms) -/// - **派生密钥缓存**:[_deriveKeyForRound] 结果按 (sessionKey, round) 缓存, -/// 同一 session 的重复加解密直接命中缓存 +/// - **RSA 解析缓存**:[_parsePublicKey] / [_parsePrivateKey] 缓存 ASN1 解析结果, +/// 同一密钥 PEM 只做一次 BigInt 构造,后续命中缓存(LRU,上限 8 条) +/// - **Session key bytes 缓存**:[_getSessionKeyBytes] 缓存 base64 → Uint8List 结果, +/// 同一 session 的多条消息只解码一次(LRU,上限 64 条) +/// - **派生密钥缓存**:[_deriveKeyForRound] 结果按 (sessionKey, round, mode) 缓存, +/// 同一 session + round 的重复加解密直接命中(LRU,上限 64 条) /// - **Random.secure() 复用**:全局单例,不再每次调用创建新实例 -/// - **KDF 双模式**:MD5(默认,UU 兼容)/ PBKDF2(可选,增强安全性) +/// - **KDF 双模式**:MD5(默认)/ PBKDF2(可选,增强安全性) +/// +/// ## 正确的接入姿势(避免重复读文件) +/// +/// 调用方(App 层)在登录后调一次 [CipherGuardSdkApi.setActiveKeyPair], +/// 把从安全存储读出的公私钥注入 SDK 内存。后续加解密使用 +/// [CipherGuardSdkApi.encryptSessionKeyWithActiveKey] / +/// [CipherGuardSdkApi.decryptSessionKeyWithActiveKey], +/// 不再每次传 key 参数,也不再重复读文件。 class EncryptionFlutterService { // ==================== 配置 ==================== - /// 密钥派生模式,默认 MD5(UU 兼容) + /// 密钥派生模式,默认 MD5 final KdfMode kdfMode; /// PBKDF2 迭代次数(仅 PBKDF2 模式有效,默认 10000) @@ -71,6 +83,8 @@ class EncryptionFlutterService { static const int sessionKeySize = 32; static const int gcmIvLength = 12; static const int _maxDerivedKeyCacheSize = 64; + static const int _maxRsaKeyCacheSize = 8; + static const int _maxSessionKeyBytesCacheSize = 64; // ==================== 性能优化:复用 Random 实例 ==================== @@ -88,6 +102,25 @@ class EncryptionFlutterService { /// 清空派生密钥缓存(session key 轮换时调用) void clearDerivedKeyCache() => _derivedKeyCache.clear(); + // ==================== 性能优化:RSA 解析缓存 ==================== + + /// RSA 公钥解析缓存:PEM -> RSAPublicKey + /// + /// RSA 密钥生命周期长(通常每设备一对),ASN1 解析 + BigInt 构造代价较高。 + /// 解析结果在内存中复用,省去重复解析开销。上限 8 条,满时淘汰最早。 + final _rsaPublicKeyCache = {}; + + /// RSA 私钥解析缓存:PEM -> RSAPrivateKey + final _rsaPrivateKeyCache = {}; + + // ==================== 性能优化:session key bytes 缓存 ==================== + + /// Session key Base64 → 字节缓存 + /// + /// _deriveKeyForRound 和 _pbkdf2Derive 每次都需要 base64Decode(sessionKey), + /// 对同一会话的多条消息重复解码。缓存后只解码一次,满时淘汰最早。 + final _sessionKeyBytesCache = {}; + // ==================== RSA 密钥管理 ==================== /// 生成 RSA 密钥对(同步,阻塞主线程) @@ -419,7 +452,7 @@ class EncryptionFlutterService { switch (kdfMode) { case KdfMode.md5: // 将 sessionKey + round 一起参与 hash,保证不同 round 产出不同密钥 - final keyBytes = base64Decode(sessionKey); + final keyBytes = _getSessionKeyBytes(sessionKey); final roundBytes = utf8.encode(':$targetRound'); final combined = Uint8List(keyBytes.length + roundBytes.length) ..setRange(0, keyBytes.length, keyBytes) @@ -449,7 +482,7 @@ class EncryptionFlutterService { /// 迭代次数由 [pbkdf2Iterations] 控制(默认 10000)。 /// 输出 16 字节(AES-128 密钥)。 Uint8List _pbkdf2Derive(String sessionKey, int targetRound) { - final keyBytes = base64Decode(sessionKey); + final keyBytes = _getSessionKeyBytes(sessionKey); final salt = utf8.encode('round:$targetRound'); final derivator = PBKDF2KeyDerivator(HMac(SHA256Digest(), 64)); @@ -460,14 +493,20 @@ class EncryptionFlutterService { return derivator.process(Uint8List.fromList(keyBytes)); } - /// 解析 RSA 公钥 PEM + /// 解析 RSA 公钥 PEM(带缓存) RSAPublicKey _parsePublicKey(String pem) { - final base64 = pem + final cached = _rsaPublicKeyCache.remove(pem); + if (cached != null) { + _rsaPublicKeyCache[pem] = cached; + return cached; + } + + final b64 = pem .replaceAll('-----BEGIN PUBLIC KEY-----', '') .replaceAll('-----END PUBLIC KEY-----', '') .replaceAll('\n', '') .trim(); - final bytes = base64Decode(base64); + final bytes = base64Decode(b64); final asn1Parser = ASN1Parser(Uint8List.fromList(bytes)); final topLevelSeq = asn1Parser.nextObject() as ASN1Sequence; @@ -480,20 +519,32 @@ class EncryptionFlutterService { final modulus = keySeq.elements[0] as ASN1Integer; final publicExponent = keySeq.elements[1] as ASN1Integer; - return RSAPublicKey( + final key = RSAPublicKey( modulus.valueAsBigInteger, publicExponent.valueAsBigInteger, ); + + if (_rsaPublicKeyCache.length >= _maxRsaKeyCacheSize) { + _rsaPublicKeyCache.remove(_rsaPublicKeyCache.keys.first); + } + _rsaPublicKeyCache[pem] = key; + return key; } - /// 解析 RSA 私钥 PEM + /// 解析 RSA 私钥 PEM(带缓存) RSAPrivateKey _parsePrivateKey(String pem) { - final base64 = pem + final cached = _rsaPrivateKeyCache.remove(pem); + if (cached != null) { + _rsaPrivateKeyCache[pem] = cached; + return cached; + } + + final b64 = pem .replaceAll('-----BEGIN PRIVATE KEY-----', '') .replaceAll('-----END PRIVATE KEY-----', '') .replaceAll('\n', '') .trim(); - final bytes = base64Decode(base64); + final bytes = base64Decode(b64); final asn1Parser = ASN1Parser(Uint8List.fromList(bytes)); final keySeq = asn1Parser.nextObject() as ASN1Sequence; @@ -503,12 +554,35 @@ class EncryptionFlutterService { final p = keySeq.elements[4] as ASN1Integer; final q = keySeq.elements[5] as ASN1Integer; - return RSAPrivateKey( + final key = RSAPrivateKey( modulus.valueAsBigInteger, privateExponent.valueAsBigInteger, p.valueAsBigInteger, q.valueAsBigInteger, ); + + if (_rsaPrivateKeyCache.length >= _maxRsaKeyCacheSize) { + _rsaPrivateKeyCache.remove(_rsaPrivateKeyCache.keys.first); + } + _rsaPrivateKeyCache[pem] = key; + return key; + } + + /// session key Base64 → 字节(带缓存) + /// + /// 同一 session key 在多条消息加解密中反复 decode,缓存后只做一次。 + Uint8List _getSessionKeyBytes(String sessionKey) { + final cached = _sessionKeyBytesCache.remove(sessionKey); + if (cached != null) { + _sessionKeyBytesCache[sessionKey] = cached; + return cached; + } + final bytes = base64Decode(sessionKey); + if (_sessionKeyBytesCache.length >= _maxSessionKeyBytesCacheSize) { + _sessionKeyBytesCache.remove(_sessionKeyBytesCache.keys.first); + } + _sessionKeyBytesCache[sessionKey] = bytes; + return bytes; } /// Hex 字符串转字节 diff --git a/packages/cipher_guard_sdk/lib/src/presentation/facade/cipher_guard_sdk_api.dart b/packages/cipher_guard_sdk/lib/src/presentation/facade/cipher_guard_sdk_api.dart index e8a56cb..45d0f86 100644 --- a/packages/cipher_guard_sdk/lib/src/presentation/facade/cipher_guard_sdk_api.dart +++ b/packages/cipher_guard_sdk/lib/src/presentation/facade/cipher_guard_sdk_api.dart @@ -65,6 +65,37 @@ abstract class CipherGuardSdkApi { required int round, }); + // ==================== 活跃密钥注册 ==================== + + /// 注册当前用户的 RSA 密钥对(登录后调用一次,内存持久化) + /// + /// 登录成功后从安全存储 / keychain 取出密钥对,调用此方法注入 SDK。 + /// 后续 [encryptSessionKeyWithActiveKey] / [decryptSessionKeyWithActiveKey] + /// 直接使用内存中的密钥,不再需要调用方每次传参,也不再重复读文件。 + /// + /// 退出登录时调用 [clearActiveKeyPair]。 + void setActiveKeyPair({ + required String publicKey, + required String privateKey, + }); + + /// 清除内存中的活跃密钥对(退出登录时调用) + void clearActiveKeyPair(); + + /// 用内存中的 RSA 公钥加密 session key + /// + /// 等价于 [encryptSessionKey],但无需每次传 publicKey。 + /// 调用前必须先调 [setActiveKeyPair],否则抛 [StateError]。 + Future encryptSessionKeyWithActiveKey({required String sessionKey}); + + /// 用内存中的 RSA 私钥解密 session key + /// + /// 等价于 [decryptSessionKey],但无需每次传 privateKey。 + /// 调用前必须先调 [setActiveKeyPair],否则抛 [StateError]。 + Future decryptSessionKeyWithActiveKey({ + required String encryptedSessionKey, + }); + // ==================== 缓存管理 ==================== /// 清空派生密钥缓存 diff --git a/packages/cipher_guard_sdk/lib/src/presentation/wiring/cipher_guard_sdk_api_impl.dart b/packages/cipher_guard_sdk/lib/src/presentation/wiring/cipher_guard_sdk_api_impl.dart index a6b5ada..4ec700c 100644 --- a/packages/cipher_guard_sdk/lib/src/presentation/wiring/cipher_guard_sdk_api_impl.dart +++ b/packages/cipher_guard_sdk/lib/src/presentation/wiring/cipher_guard_sdk_api_impl.dart @@ -10,6 +10,11 @@ class CipherGuardSdkApiImpl implements CipherGuardSdkApi { CipherGuardSdkApiImpl({required CipherGuardSdkCore core}) : _core = core; + // ── 活跃密钥内存存储(登录后 setActiveKeyPair 写入,退出登录 clearActiveKeyPair 清除)── + + String? _activePublicKey; + String? _activePrivateKey; + @override Future platformVersion() => _core.platform.getPlatformVersion(); @@ -93,6 +98,44 @@ class CipherGuardSdkApiImpl implements CipherGuardSdkApi { ); } + @override + void setActiveKeyPair({ + required String publicKey, + required String privateKey, + }) { + _activePublicKey = publicKey; + _activePrivateKey = privateKey; + } + + @override + void clearActiveKeyPair() { + _activePublicKey = null; + _activePrivateKey = null; + } + + @override + Future encryptSessionKeyWithActiveKey({required String sessionKey}) { + final key = _activePublicKey; + if (key == null) { + throw StateError('Active key pair not set. Call setActiveKeyPair first.'); + } + return encryptSessionKey(sessionKey: sessionKey, publicKey: key); + } + + @override + Future decryptSessionKeyWithActiveKey({ + required String encryptedSessionKey, + }) { + final key = _activePrivateKey; + if (key == null) { + throw StateError('Active key pair not set. Call setActiveKeyPair first.'); + } + return decryptSessionKey( + encryptedSessionKey: encryptedSessionKey, + privateKey: key, + ); + } + @override void clearDerivedKeyCache() => _core.encryptionRepo.clearDerivedKeyCache();