feat(e2e): 端对端加密完全对齐老项目 — cipher_guard_sdk 修正 + EncryptionManager 集成
Some checks failed
CI / Lint (push) Has been cancelled
Some checks failed
CI / Lint (push) Has been cancelled
修正 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>
This commit is contained in:
170
apps/im_app/lib/data/remote/cipher_api_requests.dart
Normal file
170
apps/im_app/lib/data/remote/cipher_api_requests.dart
Normal file
@@ -0,0 +1,170 @@
|
||||
import 'package:networks_sdk/networks_sdk.dart';
|
||||
|
||||
/// 对齐老项目 /app/api/cipher/v2/* API 端点
|
||||
///
|
||||
/// /app/api/cipher/v2/key/set 是预发布接口,仅测试阶段使用。
|
||||
|
||||
// ── GET /app/api/cipher/v2/key/my — 获取自己的公钥 ────────────────────────
|
||||
|
||||
class CipherMyKeyResponse {
|
||||
final String publicKey;
|
||||
final String? encPrivate;
|
||||
final int? uid;
|
||||
|
||||
const CipherMyKeyResponse({
|
||||
required this.publicKey,
|
||||
this.encPrivate,
|
||||
this.uid,
|
||||
});
|
||||
|
||||
factory CipherMyKeyResponse.fromJson(Map<String, dynamic> json) =>
|
||||
CipherMyKeyResponse(
|
||||
publicKey: (json['public_key'] ?? '') as String,
|
||||
encPrivate: json['enc_pk'] as String?,
|
||||
uid: json['uid'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
class CipherGetMyKeyRequest extends ApiRequestable<CipherMyKeyResponse?> {
|
||||
@override
|
||||
String get path => '/app/api/cipher/v2/key/my';
|
||||
|
||||
@override
|
||||
HttpMethod get method => HttpMethod.get;
|
||||
|
||||
@override
|
||||
Map<String, dynamic> get parameters => {};
|
||||
|
||||
@override
|
||||
CipherMyKeyResponse? decodeResponse(dynamic response) {
|
||||
final data = (response as dynamic).data;
|
||||
if (data is! Map<String, dynamic>) return null;
|
||||
return CipherMyKeyResponse.fromJson(data);
|
||||
}
|
||||
}
|
||||
|
||||
// ── POST /app/api/cipher/v2/key/set — 上传公钥 ─────────────────────────────
|
||||
|
||||
class CipherSetKeyRequest extends ApiRequestable<void> {
|
||||
final String publicKey;
|
||||
final String encPk;
|
||||
|
||||
CipherSetKeyRequest({required this.publicKey, required this.encPk});
|
||||
|
||||
@override
|
||||
String get path => '/app/api/cipher/v2/key/set';
|
||||
|
||||
@override
|
||||
HttpMethod get method => HttpMethod.post;
|
||||
|
||||
@override
|
||||
Map<String, dynamic> get parameters => {
|
||||
'public_key': publicKey,
|
||||
'enc_pk': encPk,
|
||||
};
|
||||
|
||||
@override
|
||||
void decodeResponse(dynamic response) {}
|
||||
}
|
||||
|
||||
// ── GET /app/api/cipher/v2/chat/my — 获取所有聊天的加密密钥 ─────────────────
|
||||
|
||||
class CipherChatKeyItem {
|
||||
final int? chatId;
|
||||
final String? session;
|
||||
final int? round;
|
||||
|
||||
const CipherChatKeyItem({this.chatId, this.session, this.round});
|
||||
|
||||
factory CipherChatKeyItem.fromJson(Map<String, dynamic> json) =>
|
||||
CipherChatKeyItem(
|
||||
chatId: json['chat_id'] as int?,
|
||||
session: json['session'] as String?,
|
||||
round: json['round'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
class CipherGetMyChatKeysRequest
|
||||
extends ApiRequestable<List<CipherChatKeyItem>?> {
|
||||
@override
|
||||
String get path => '/app/api/cipher/v2/chat/my';
|
||||
|
||||
@override
|
||||
HttpMethod get method => HttpMethod.get;
|
||||
|
||||
@override
|
||||
Map<String, dynamic> get parameters => {};
|
||||
|
||||
@override
|
||||
List<CipherChatKeyItem>? decodeResponse(dynamic response) {
|
||||
final data = (response as dynamic).data;
|
||||
if (data is! List) return null;
|
||||
return data
|
||||
.cast<Map<String, dynamic>>()
|
||||
.map(CipherChatKeyItem.fromJson)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
// ── GET /app/api/cipher/v2/key/gets — 获取其他用户的公钥 ─────────────────────
|
||||
|
||||
class CipherUserKeyResponse {
|
||||
final int? uid;
|
||||
final String? publicKey;
|
||||
|
||||
const CipherUserKeyResponse({this.uid, this.publicKey});
|
||||
|
||||
factory CipherUserKeyResponse.fromJson(Map<String, dynamic> json) =>
|
||||
CipherUserKeyResponse(
|
||||
uid: json['uid'] as int?,
|
||||
publicKey: json['public_key'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
class CipherGetUsersKeysRequest
|
||||
extends ApiRequestable<List<CipherUserKeyResponse>?> {
|
||||
final List<int> userIds;
|
||||
|
||||
CipherGetUsersKeysRequest({required this.userIds});
|
||||
|
||||
@override
|
||||
String get path => '/app/api/cipher/v2/key/gets';
|
||||
|
||||
@override
|
||||
HttpMethod get method => HttpMethod.get;
|
||||
|
||||
@override
|
||||
Map<String, dynamic> get parameters => {
|
||||
'uids': userIds.join(','),
|
||||
};
|
||||
|
||||
@override
|
||||
List<CipherUserKeyResponse>? decodeResponse(dynamic response) {
|
||||
final data = (response as dynamic).data;
|
||||
if (data is! List) return null;
|
||||
return data
|
||||
.cast<Map<String, dynamic>>()
|
||||
.map(CipherUserKeyResponse.fromJson)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
// ── POST /app/api/cipher/v2/chat/update — 更新聊天加密密钥 ──────────────────
|
||||
|
||||
class CipherUpdateChatKeysRequest extends ApiRequestable<void> {
|
||||
final List<Map<String, dynamic>> sessions;
|
||||
|
||||
CipherUpdateChatKeysRequest({required this.sessions});
|
||||
|
||||
@override
|
||||
String get path => '/app/api/cipher/v2/chat/update';
|
||||
|
||||
@override
|
||||
HttpMethod get method => HttpMethod.post;
|
||||
|
||||
@override
|
||||
Map<String, dynamic> get parameters => {'sessions': sessions};
|
||||
|
||||
@override
|
||||
void decodeResponse(dynamic response) {}
|
||||
}
|
||||
Reference in New Issue
Block a user