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();