Files
customer-im-client-dev/apps/im_app/lib/app/di/app_providers.dart
pp-bot 52a3f0f45c
Some checks failed
CI / Lint (push) Has been cancelled
feat(e2e): 端对端加密完全对齐老项目 — cipher_guard_sdk 修正 + EncryptionManager 集成
修正 cipher_guard_sdk 4 个关键密码学差异使其与老 Flutter 项目 (im-client-im-dev) 和 iOS
EncryptionManager 完全互操作:

1. AES: 显式 SIC/CTR 模式 + 16 zero-byte IV(原 SDK 用随机 IV + KDF 派生密钥)
2. RSA: bare RSAEngine 无 PKCS1 padding(原 SDK 用 PKCS1Encoding)
3. Session key: 32-char alphanumeric ASCII(原 SDK 用 base64 random bytes)
4. Wire format: base64(ciphertext) 无 IV 前缀

新增 EncryptionManager:
- Per-chat round-based key chain(最多 10 rounds/chat,FIFO 淘汰)
- 登录后自动 setup:RSA 密钥对生成/存储 + 公钥上传 + chat 密钥拉取解密
- API 集成:cipher/v2/key/my, key/set, chat/my
- 消息加密返回 JSON envelope {"round":N,"data":"<base64>"}
- 消息解密兼容 JSON envelope + legacy raw base64

集成到消息流:
- SendMessageUseCase: 发送前加密 content → wireContent
- WsMessageService: 收到消息后解密 content + lastMsg
- 无密钥时 fallback 到明文(对齐 iOS 行为)

注意:/app/api/cipher/v2/key/set 仍为预发布接口,仅测试阶段使用

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:44:00 +09:00

189 lines
6.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:cipher_guard_sdk/cipher_guard_sdk.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:im_app/core/foundation/device_info.dart';
import 'package:im_app/core/services/app_initializer.dart';
import 'package:im_app/core/services/encryption_manager.dart';
import 'package:im_app/app/di/network_provider.dart';
// ── 认证 ──────────────────────────────────────────────────────────────────────
/// 登录状态管理
///
/// 同时继承 [ChangeNotifier],作为 go_router [GoRouter.refreshListenable] 使用,
/// 登录 / 退出时 go_router 自动重新执行 redirect无需手动触发。
///
/// ## 当前状态
///
/// Demo 实现无持久化。storage_sdk 就绪后替换为:
/// - `build`:从安全存储读取 token有则视为已登录
/// - `login` / `logout`:同步更新安全存储
class AuthNotifier extends ChangeNotifier {
bool _isLoggedIn = false;
int? _currentUid;
bool get isLoggedIn => _isLoggedIn;
/// 登录用户的 UID登录成功后由 LoginViewModel 写入
int? get currentUid => _currentUid;
/// E2E EncryptionManager — 由外部注入(见 LoginViewModel
EncryptionManager? _encryptionManager;
NetworksSdkApi? _api;
void setEncryptionDeps(EncryptionManager encMgr, NetworksSdkApi api) {
_encryptionManager = encMgr;
_api = api;
}
void login({required int uid}) {
_isLoggedIn = true;
_currentUid = uid;
notifyListeners();
// E2E setup: 对齐 iOS AppCoordinator.onLogin → EncryptionManager.setup()
if (_encryptionManager != null && _api != null) {
_encryptionManager!.setup(_api!);
}
}
void logout() {
_isLoggedIn = false;
_currentUid = null;
// E2E teardown: 清除所有加密密钥
_encryptionManager?.clearKeys();
notifyListeners();
}
}
/// 登录状态 Provider
///
/// 自动注入 EncryptionManager + API clientlogin() 后自动触发 E2E setup。
final authNotifierProvider = Provider<AuthNotifier>((ref) {
final auth = AuthNotifier();
auth.setEncryptionDeps(
ref.read(encryptionManagerProvider),
ref.read(networkSdkApiProvider),
);
return auth;
});
// ── E2E 加密 ────────────────────────────────────────────────────────────────
/// CipherGuardSdkApi 单例 — 对齐老项目加密引擎
final cipherSdkProvider = Provider<CipherGuardSdkApi>((ref) {
return CipherGuardSdkApi();
});
/// EncryptionManager 单例 — per-chat key chain + API integration
///
/// 登录后调用 `encMgr.setup(api)` 启动 E2E退出时调用 `encMgr.clearKeys()`。
/// 对齐 iOS EncryptionManager + 老项目 EncryptionMgr。
final encryptionManagerProvider = Provider<EncryptionManager>((ref) {
return EncryptionManager(cipherSdk: ref.read(cipherSdkProvider));
});
// ── 主题 ──────────────────────────────────────────────────────────────────────
/// 主题模式 Notifier — 控制应用全局亮 / 暗主题
///
/// 启动时从持久化存储读取上次保存的主题模式,无则默认跟随系统。
/// 切换时先更新内存状态,再写入持久化存储。
///
/// ## storage_sdk 接入步骤
///
/// 1. 在 `build()` 解开 TODO读取存储值作为初始模式
/// 2. 在 `setMode()` 解开 TODO每次切换后写入存储
/// 3. 若存储接口是异步的,将 `Notifier<ThemeMode>` 改为
/// `AsyncNotifier<ThemeMode>``build()` 改为 `Future<ThemeMode>`
class ThemeModeNotifier extends Notifier<ThemeMode> {
@override
ThemeMode build() {
// TODO: storage_sdk 就绪后从持久化读取初始值:
// final saved = ref.read(themeStorageProvider).readThemeMode();
// return saved ?? ThemeMode.system;
return ThemeMode.system;
}
void setMode(ThemeMode mode) {
state = mode;
// TODO: storage_sdk 就绪后写入持久化:
// ref.read(themeStorageProvider).saveThemeMode(mode);
}
}
/// 主题模式 Provider
///
/// ## Setting 页切换(只需一行)
///
/// ```dart
/// ref.read(themeModeProvider.notifier).setMode(ThemeMode.system);
/// ref.read(themeModeProvider.notifier).setMode(ThemeMode.light);
/// ref.read(themeModeProvider.notifier).setMode(ThemeMode.dark);
/// ```
///
/// ## 持久化storage_sdk TODO
///
/// 读取和写入的 TODO 均在 [ThemeModeNotifier] 内,接入 storage_sdk 后解开即可。
final themeModeProvider = NotifierProvider<ThemeModeNotifier, ThemeMode>(
ThemeModeNotifier.new,
);
// ── 启动初始化 ────────────────────────────────────────────────────────────────
/// AppInitializer Provider
///
/// 集中声明所有启动初始化任务app.dart 只需一行 `.run()`。
///
/// ## 任务分类规则
///
/// 问自己:「这个任务不完成,用户能正常看到首页吗?」
/// - **不能** → 放 critical谨慎每多一个都拖慢启动
/// - **能** → 放 deferred绝大多数情况
///
/// ## 当前任务清单
///
/// | 阶段 | 任务 | 说明 |
/// |---|---|---|
/// | Critical | NetworkMonitor | 后续 HTTP、WebSocket 都依赖网络状态 |
/// | Deferred | (待扩展) | 推送注册、登录态恢复、缓存预热等 |
final appInitializerProvider = Provider<AppInitializer>((ref) {
return AppInitializer(
onLog: (message, {tag}) {
// ignore: avoid_print
print('[${tag ?? 'AppInit'}] $message');
},
critical: [
// 网络监听必须最先就绪(后续 HTTP、WebSocket 都依赖它)
InitTask(
name: 'NetworkMonitor',
task: () => ref.read(networkMonitorProvider).initialize(),
),
// 预取设备 ID / 设备名platformHeaders 同步读取
InitTask(
name: 'DeviceInfo',
task: DeviceInfo.init,
),
],
deferred: [
// TODO: 推送注册
// InitTask(
// name: 'PushNotification',
// task: () => ref.read(pushServiceProvider).register(),
// ),
//
// TODO: 登录态恢复(从安全存储读取 token → 自动登录)
// InitTask(
// name: 'AuthRestore',
// task: () => ref.read(authRestoreUseCaseProvider).execute(),
// ),
//
// TODO: 缓存预热
// InitTask(
// name: 'CacheWarmup',
// task: () => ref.read(cacheServiceProvider).warmup(),
// ),
],
);
});