Initial project
This commit is contained in:
15
packages/cipher_guard_sdk/lib/cipher_guard_sdk.dart
Normal file
15
packages/cipher_guard_sdk/lib/cipher_guard_sdk.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
/// CipherGuard SDK - 端对端加密 Flutter 插件
|
||||
///
|
||||
/// 提供完整的加密/解密功能:
|
||||
/// - RSA 密钥对管理(用户级别)
|
||||
/// - AES 会话密钥管理(聊天室级别)
|
||||
/// - 消息加密/解密
|
||||
/// - 原生平台同步(iOS App Group)
|
||||
/// - 推送通知解密
|
||||
library;
|
||||
|
||||
export 'src/presentation/facade/cipher_guard_sdk_api.dart';
|
||||
export 'src/domain/entities/rsa_key_pair.dart';
|
||||
export 'src/domain/entities/session_key.dart';
|
||||
export 'src/domain/entities/encrypted_message.dart';
|
||||
export 'src/domain/entities/chat_encryption_key.dart';
|
||||
@@ -0,0 +1,25 @@
|
||||
/// Method Channel 常量
|
||||
/// 定義 Flutter 與原生平台通訊的方法名稱
|
||||
///
|
||||
/// 注意:加密邏輯已移至 Flutter 端
|
||||
/// 原生端僅保留同步功能
|
||||
class EncryptionMethodChannel {
|
||||
// ==================== Channel 配置 ====================
|
||||
|
||||
/// Channel 名稱
|
||||
static const String channelName = 'cipher_guard_sdk/e2e';
|
||||
|
||||
// ==================== 同步功能 (保留在原生端) ====================
|
||||
|
||||
/// 同步單個聊天室金鑰到原生
|
||||
static const String syncEncryptionKey = 'syncEncryptionKey';
|
||||
|
||||
/// 批量同步所有聊天室金鑰
|
||||
static const String syncAllEncryptionKeys = 'syncAllEncryptionKeys';
|
||||
|
||||
// ==================== 配置相關 ====================
|
||||
|
||||
/// 獲取 SDK 版本
|
||||
static const String getPlatformVersion = 'getPlatformVersion';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,471 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:asn1lib/asn1lib.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:encrypt/encrypt.dart' as encrypt_pkg;
|
||||
import 'package:pointycastle/api.dart';
|
||||
import 'package:pointycastle/asymmetric/api.dart';
|
||||
import 'package:pointycastle/key_generators/api.dart';
|
||||
import 'package:pointycastle/key_generators/rsa_key_generator.dart';
|
||||
import 'package:pointycastle/random/fortuna_random.dart';
|
||||
import 'package:pointycastle/asymmetric/rsa.dart';
|
||||
import 'package:pointycastle/asymmetric/pkcs1.dart';
|
||||
|
||||
/// Flutter Encryption Service
|
||||
/// Implements all encryption logic in Flutter using pointycastle and encrypt packages
|
||||
/// Replaces native Android/iOS encryption implementations
|
||||
class EncryptionFlutterService {
|
||||
// ==================== Constants ====================
|
||||
static const int sessionKeySize = 32;
|
||||
static const int gcmIvLength = 12;
|
||||
|
||||
// ==================== RSA Key Management ====================
|
||||
|
||||
/// Generate RSA key pair in PEM format
|
||||
RsaKeyPairResult generateRsaKeyPair({int keySize = 1024}) {
|
||||
try {
|
||||
// Get secure random
|
||||
final secureRandom = FortunaRandom();
|
||||
secureRandom.seed(KeyParameter(_generateSecureRandomBytes(32)));
|
||||
|
||||
// Create RSA key generator
|
||||
final keyGen = RSAKeyGenerator();
|
||||
keyGen.init(ParametersWithRandom(
|
||||
RSAKeyGeneratorParameters(BigInt.parse('65537'), keySize, 64),
|
||||
secureRandom,
|
||||
));
|
||||
|
||||
// Generate key pair
|
||||
final keyPair = keyGen.generateKeyPair();
|
||||
final rsaPublicKey = keyPair.publicKey as RSAPublicKey;
|
||||
final rsaPrivateKey = keyPair.privateKey as RSAPrivateKey;
|
||||
|
||||
// Export to PEM format
|
||||
final publicKeyPem = _encodeRSAPublicKey(rsaPublicKey);
|
||||
final privateKeyPem = _encodeRSAPrivateKey(rsaPrivateKey);
|
||||
|
||||
return RsaKeyPairResult(
|
||||
publicKey: publicKeyPem,
|
||||
privateKey: privateKeyPem,
|
||||
);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to generate RSA key pair: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode RSA public key to PEM format using asn1lib
|
||||
String _encodeRSAPublicKey(RSAPublicKey publicKey) {
|
||||
// Build RSAPublicKeyInfo structure
|
||||
final topSeq = ASN1Sequence();
|
||||
|
||||
// AlgorithmIdentifier: OID 1.2.840.113549.1.1.1 + NULL
|
||||
final algoSeq = ASN1Sequence();
|
||||
algoSeq.add(ASN1ObjectIdentifier([1, 2, 840, 113549, 1, 1, 1])); // RSA
|
||||
algoSeq.add(ASN1Null());
|
||||
topSeq.add(algoSeq);
|
||||
|
||||
// RSAPublicKey: modulus + publicExponent
|
||||
final keySeq = ASN1Sequence();
|
||||
keySeq.add(ASN1Integer(publicKey.n!));
|
||||
keySeq.add(ASN1Integer(publicKey.exponent!));
|
||||
|
||||
// BitString wrapping the key (with 0 unused bits prefix)
|
||||
final keyBytes = keySeq.encodedBytes;
|
||||
final keyList = List<int>.from(keyBytes);
|
||||
keyList.insert(0, 0); // Add unused bits byte
|
||||
topSeq.add(ASN1BitString(keyList));
|
||||
|
||||
final derBytes = topSeq.encodedBytes;
|
||||
final base64 = base64Encode(derBytes.toList());
|
||||
return '-----BEGIN PUBLIC KEY-----\n$base64\n-----END PUBLIC KEY-----';
|
||||
}
|
||||
|
||||
/// Encode RSA private key to PEM format using asn1lib
|
||||
String _encodeRSAPrivateKey(RSAPrivateKey privateKey) {
|
||||
// Build RSAPrivateKey structure (PKCS#8 format)
|
||||
final topSeq = ASN1Sequence();
|
||||
|
||||
// Version (0)
|
||||
topSeq.add(ASN1Integer(BigInt.zero));
|
||||
|
||||
// Modulus
|
||||
topSeq.add(ASN1Integer(privateKey.n!));
|
||||
|
||||
// Public Exponent
|
||||
topSeq.add(ASN1Integer(privateKey.exponent!));
|
||||
|
||||
// Private Exponent
|
||||
topSeq.add(ASN1Integer(privateKey.privateExponent!));
|
||||
|
||||
// Prime P
|
||||
topSeq.add(ASN1Integer(privateKey.p!));
|
||||
|
||||
// Prime Q
|
||||
topSeq.add(ASN1Integer(privateKey.q!));
|
||||
|
||||
// (Optional CRT params omitted for simplicity)
|
||||
|
||||
final derBytes = topSeq.encodedBytes;
|
||||
final base64 = base64Encode(derBytes.toList());
|
||||
return '-----BEGIN PRIVATE KEY-----\n$base64\n-----END PRIVATE KEY-----';
|
||||
}
|
||||
|
||||
// ==================== Private Key Encryption/Decryption ====================
|
||||
|
||||
/// Encrypt private key with password (AES-CBC with MD5-derived key)
|
||||
String encryptPrivateKey({
|
||||
required String privateKey,
|
||||
required String password,
|
||||
}) {
|
||||
try {
|
||||
// Generate AES key from MD5(password)
|
||||
final aesKey = _md5Hash(password);
|
||||
|
||||
// Generate random IV (16 bytes)
|
||||
final iv = _generateSecureRandomBytes(16);
|
||||
|
||||
// AES encrypt using encrypt package
|
||||
final secretKey = encrypt_pkg.Key(aesKey);
|
||||
final encryptor = encrypt_pkg.Encrypter(
|
||||
encrypt_pkg.AES(secretKey, mode: encrypt_pkg.AESMode.cbc),
|
||||
);
|
||||
|
||||
final encrypted = encryptor.encrypt(privateKey, iv: encrypt_pkg.IV(iv));
|
||||
final encryptedBytes = encrypted.bytes;
|
||||
|
||||
// Combine IV + encrypted data
|
||||
final combined = Uint8List(iv.length + encryptedBytes.length);
|
||||
combined.setAll(0, iv);
|
||||
combined.setAll(iv.length, encryptedBytes);
|
||||
|
||||
return base64Encode(combined);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to encrypt private key: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrypt private key with password (AES-CBC with MD5-derived key)
|
||||
String decryptPrivateKey({
|
||||
required String encryptedPrivateKey,
|
||||
required String password,
|
||||
}) {
|
||||
try {
|
||||
// Generate AES key from MD5(password)
|
||||
final aesKey = _md5Hash(password);
|
||||
|
||||
// Decode Base64
|
||||
final combined = base64Decode(encryptedPrivateKey);
|
||||
|
||||
// Extract IV and encrypted data
|
||||
final iv = combined.sublist(0, 16);
|
||||
final encBytes = combined.sublist(16);
|
||||
|
||||
// AES decrypt
|
||||
final secretKey = encrypt_pkg.Key(aesKey);
|
||||
final encryptor = encrypt_pkg.Encrypter(
|
||||
encrypt_pkg.AES(secretKey, mode: encrypt_pkg.AESMode.cbc),
|
||||
);
|
||||
|
||||
final decrypted = encryptor.decrypt(
|
||||
encrypt_pkg.Encrypted(encBytes),
|
||||
iv: encrypt_pkg.IV(iv),
|
||||
);
|
||||
|
||||
return decrypted;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to decrypt private key: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Session Key Management ====================
|
||||
|
||||
/// Generate session key (32 bytes random)
|
||||
SessionKeyResult generateSessionKey({int initialRound = 1}) {
|
||||
final keyBytes = _generateSecureRandomBytes(sessionKeySize);
|
||||
final key = base64Encode(keyBytes);
|
||||
|
||||
return SessionKeyResult(
|
||||
key: key,
|
||||
round: initialRound,
|
||||
);
|
||||
}
|
||||
|
||||
/// Encrypt session key with RSA public key
|
||||
String encryptSessionKey({
|
||||
required String sessionKey,
|
||||
required String publicKey,
|
||||
}) {
|
||||
try {
|
||||
// Parse RSA public key
|
||||
final rsaPublicKey = _parsePublicKey(publicKey);
|
||||
|
||||
// RSA encrypt using PKCS1 padding (like native implementations)
|
||||
final cipher = PKCS1Encoding(RSAEngine());
|
||||
cipher.init(true, PublicKeyParameter<RSAPublicKey>(rsaPublicKey));
|
||||
|
||||
final encryptedBytes = cipher.process(utf8.encode(sessionKey));
|
||||
return base64Encode(encryptedBytes);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to encrypt session key: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrypt session key with RSA private key
|
||||
String decryptSessionKey({
|
||||
required String encryptedSessionKey,
|
||||
required String privateKey,
|
||||
}) {
|
||||
try {
|
||||
// Parse RSA private key
|
||||
final rsaPrivateKey = _parsePrivateKey(privateKey);
|
||||
|
||||
// RSA decrypt using PKCS1 padding (like native implementations)
|
||||
final cipher = PKCS1Encoding(RSAEngine());
|
||||
cipher.init(false, PrivateKeyParameter<RSAPrivateKey>(rsaPrivateKey));
|
||||
|
||||
final decryptedBytes = cipher.process(base64Decode(encryptedSessionKey));
|
||||
return utf8.decode(decryptedBytes);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to decrypt session key: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Message Encryption/Decryption ====================
|
||||
|
||||
/// Encrypt message (AES-CTR with round-based key derivation)
|
||||
EncryptedMessageResult encryptMessage({
|
||||
required String plaintext,
|
||||
required String sessionKey,
|
||||
required int round,
|
||||
}) {
|
||||
try {
|
||||
// Derive key for round
|
||||
final actualKey = _deriveKeyForRound(sessionKey, round);
|
||||
|
||||
// Generate random IV (16 bytes for CTR)
|
||||
final iv = _generateSecureRandomBytes(16);
|
||||
|
||||
// AES-CTR encrypt
|
||||
final secretKey = encrypt_pkg.Key(actualKey);
|
||||
final encryptor = encrypt_pkg.Encrypter(
|
||||
encrypt_pkg.AES(secretKey, mode: encrypt_pkg.AESMode.ctr),
|
||||
);
|
||||
|
||||
final encrypted = encryptor.encrypt(plaintext, iv: encrypt_pkg.IV(iv));
|
||||
final encryptedBytes = encrypted.bytes;
|
||||
|
||||
// Combine IV + encrypted data
|
||||
final combined = Uint8List(iv.length + encryptedBytes.length);
|
||||
combined.setAll(0, iv);
|
||||
combined.setAll(iv.length, encryptedBytes);
|
||||
|
||||
final data = base64Encode(combined);
|
||||
|
||||
return EncryptedMessageResult(
|
||||
round: round,
|
||||
data: data,
|
||||
);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to encrypt message: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrypt message (AES-CTR with round-based key derivation)
|
||||
String decryptMessage({
|
||||
required String encryptedData,
|
||||
required String sessionKey,
|
||||
required int round,
|
||||
}) {
|
||||
try {
|
||||
// Derive key for round
|
||||
final actualKey = _deriveKeyForRound(sessionKey, round);
|
||||
|
||||
// Decode Base64
|
||||
final combined = base64Decode(encryptedData);
|
||||
|
||||
// Extract IV and encrypted data
|
||||
final iv = combined.sublist(0, 16);
|
||||
final encBytes = combined.sublist(16);
|
||||
|
||||
// AES-CTR decrypt
|
||||
final secretKey = encrypt_pkg.Key(actualKey);
|
||||
final encryptor = encrypt_pkg.Encrypter(
|
||||
encrypt_pkg.AES(secretKey, mode: encrypt_pkg.AESMode.ctr),
|
||||
);
|
||||
|
||||
final decrypted = encryptor.decrypt(
|
||||
encrypt_pkg.Encrypted(encBytes),
|
||||
iv: encrypt_pkg.IV(iv),
|
||||
);
|
||||
|
||||
return decrypted;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to decrypt message: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Push Notification Decryption ====================
|
||||
|
||||
/// Set AES secret for push notification decryption
|
||||
void setAesSecret(String aesSecret) {
|
||||
_aesSecret = aesSecret;
|
||||
}
|
||||
|
||||
String? _aesSecret;
|
||||
|
||||
/// Decrypt push notification (AES-GCM)
|
||||
String decryptPushNotification({
|
||||
required String encryptedData,
|
||||
}) {
|
||||
try {
|
||||
final secret = _aesSecret;
|
||||
if (secret == null) {
|
||||
throw Exception('AES_SECRET not set');
|
||||
}
|
||||
|
||||
// Convert hex string to bytes
|
||||
final secretBytes = _hexStringToBytes(secret);
|
||||
|
||||
// Decode Base64
|
||||
final combined = base64Decode(encryptedData);
|
||||
|
||||
// Extract IV and encrypted data
|
||||
final iv = combined.sublist(0, gcmIvLength);
|
||||
final encBytes = combined.sublist(gcmIvLength);
|
||||
|
||||
// AES-GCM decrypt
|
||||
final secretKey = encrypt_pkg.Key(secretBytes);
|
||||
final encryptor = encrypt_pkg.Encrypter(
|
||||
encrypt_pkg.AES(secretKey, mode: encrypt_pkg.AESMode.gcm),
|
||||
);
|
||||
|
||||
final decrypted = encryptor.decrypt(
|
||||
encrypt_pkg.Encrypted(encBytes),
|
||||
iv: encrypt_pkg.IV(iv),
|
||||
);
|
||||
|
||||
return decrypted;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to decrypt push notification: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Helper Methods ====================
|
||||
|
||||
/// Generate secure random bytes
|
||||
Uint8List _generateSecureRandomBytes(int length) {
|
||||
final random = Random.secure();
|
||||
final bytes = Uint8List(length);
|
||||
for (var i = 0; i < length; i++) {
|
||||
bytes[i] = random.nextInt(256);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// MD5 hash
|
||||
Uint8List _md5Hash(String input) {
|
||||
final bytes = utf8.encode(input);
|
||||
final hash = md5.convert(bytes).bytes as Uint8List;
|
||||
return hash;
|
||||
}
|
||||
|
||||
/// Derive key for round (MD5 hash of session key)
|
||||
Uint8List _deriveKeyForRound(String sessionKey, int targetRound) {
|
||||
// Base64 decode session key
|
||||
final keyBytes = base64Decode(sessionKey);
|
||||
|
||||
// Apply MD5 for the round (simplified version)
|
||||
final hash = md5.convert(keyBytes).bytes as Uint8List;
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
/// Parse RSA public key from PEM
|
||||
RSAPublicKey _parsePublicKey(String pem) {
|
||||
final base64 = pem
|
||||
.replaceAll('-----BEGIN PUBLIC KEY-----', '')
|
||||
.replaceAll('-----END PUBLIC KEY-----', '')
|
||||
.replaceAll('\n', '')
|
||||
.trim();
|
||||
final bytes = base64Decode(base64);
|
||||
|
||||
// Parse ASN.1 DER format
|
||||
final asn1Parser = ASN1Parser(Uint8List.fromList(bytes));
|
||||
final topLevelSeq = asn1Parser.nextObject() as ASN1Sequence;
|
||||
|
||||
final subjectPublicKeyInfo = topLevelSeq.elements[1] as ASN1BitString;
|
||||
final keyBytes = subjectPublicKeyInfo.contentBytes();
|
||||
final keyParser = ASN1Parser(Uint8List.fromList(keyBytes));
|
||||
final keySeq = keyParser.nextObject() as ASN1Sequence;
|
||||
|
||||
final modulus = keySeq.elements[0] as ASN1Integer;
|
||||
final publicExponent = keySeq.elements[1] as ASN1Integer;
|
||||
|
||||
return RSAPublicKey(
|
||||
modulus.valueAsBigInteger,
|
||||
publicExponent.valueAsBigInteger,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parse RSA private key from PEM
|
||||
RSAPrivateKey _parsePrivateKey(String pem) {
|
||||
final base64 = pem
|
||||
.replaceAll('-----BEGIN PRIVATE KEY-----', '')
|
||||
.replaceAll('-----END PRIVATE KEY-----', '')
|
||||
.replaceAll('\n', '')
|
||||
.trim();
|
||||
final bytes = base64Decode(base64);
|
||||
|
||||
// Parse ASN.1 DER format
|
||||
final asn1Parser = ASN1Parser(Uint8List.fromList(bytes));
|
||||
final keySeq = asn1Parser.nextObject() as ASN1Sequence;
|
||||
|
||||
final modulus = keySeq.elements[1] as ASN1Integer;
|
||||
final privateExponent = keySeq.elements[3] as ASN1Integer;
|
||||
final p = keySeq.elements[4] as ASN1Integer;
|
||||
final q = keySeq.elements[5] as ASN1Integer;
|
||||
|
||||
return RSAPrivateKey(
|
||||
modulus.valueAsBigInteger,
|
||||
privateExponent.valueAsBigInteger,
|
||||
p.valueAsBigInteger,
|
||||
q.valueAsBigInteger,
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert hex string to bytes
|
||||
Uint8List _hexStringToBytes(String hex) {
|
||||
final len = hex.length;
|
||||
final data = Uint8List(len ~/ 2);
|
||||
for (var i = 0; i < len; i += 2) {
|
||||
data[i ~/ 2] = (int.parse(hex[i], radix: 16) << 4) + int.parse(hex[i + 1], radix: 16);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Result Classes ====================
|
||||
|
||||
class RsaKeyPairResult {
|
||||
final String publicKey;
|
||||
final String privateKey;
|
||||
|
||||
RsaKeyPairResult({required this.publicKey, required this.privateKey});
|
||||
}
|
||||
|
||||
class SessionKeyResult {
|
||||
final String key;
|
||||
final int round;
|
||||
|
||||
SessionKeyResult({required this.key, required this.round});
|
||||
}
|
||||
|
||||
class EncryptedMessageResult {
|
||||
final int round;
|
||||
final String data;
|
||||
|
||||
EncryptedMessageResult({required this.round, required this.data});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cipher_guard_sdk/src/data/constants/method_channel_constants.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../dto/method_channel_response.dart';
|
||||
|
||||
// ==================== Method Channel 資料來源 ====================
|
||||
|
||||
/// Method Channel 資料來源
|
||||
/// 負責與原生平台 (iOS/Android) 通訊
|
||||
///
|
||||
/// 注意:加密邏輯移至 Flutter 端
|
||||
/// 原生端僅保留同步功能
|
||||
class EncryptionMethodChannelDataSource {
|
||||
final MethodChannel _channel;
|
||||
|
||||
EncryptionMethodChannelDataSource() : _channel = const MethodChannel(EncryptionMethodChannel.channelName);
|
||||
|
||||
// ==================== 內部響應解析 ====================
|
||||
|
||||
/// 標準化響應解析
|
||||
/// 解析原生平台返回的標準化響應格式
|
||||
/// 格式: {"success": bool, "data": {...}, "error": String?}
|
||||
MethodChannelResponse<Map<String, dynamic>> _parseResponse(Object? result) {
|
||||
if (result == null) {
|
||||
return MethodChannelResponse.failure('Null response from native');
|
||||
}
|
||||
|
||||
if (result is Map<String, dynamic>) {
|
||||
return _parseStandardResponse(result);
|
||||
}
|
||||
|
||||
if (result is Map<Object?, Object?>) {
|
||||
var mapResult = _coerceToMapStringDynamic(result);
|
||||
return _parseStandardResponse(mapResult);
|
||||
}
|
||||
|
||||
// 嘗試解析 JSON 字串
|
||||
if (result is String && result.startsWith('{')) {
|
||||
try {
|
||||
final decoded = jsonDecode(result);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
return _parseStandardResponse(decoded);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// 不支援的格式
|
||||
return MethodChannelResponse.failure(
|
||||
'Unsupported response format: ${result.runtimeType}',
|
||||
);
|
||||
}
|
||||
|
||||
/// 解析標準化響應格式
|
||||
MethodChannelResponse<Map<String, dynamic>> _parseStandardResponse(Map<String, dynamic> map) {
|
||||
final success = map['success'] == "true";
|
||||
final data = map['data'];
|
||||
final error = map['error'];
|
||||
final decodedResult = jsonDecode(data);
|
||||
if (success) {
|
||||
return MethodChannelResponse.success(decodedResult);
|
||||
}
|
||||
|
||||
return MethodChannelResponse<Map<String, dynamic>>(
|
||||
isSuccess: success,
|
||||
data: null,
|
||||
error: error?.toString() ?? (success ? null : 'Unknown error'),
|
||||
);
|
||||
}
|
||||
|
||||
/// 將任意回傳結果轉成 `Map<String, dynamic>`
|
||||
Map<String, dynamic> _coerceToMapStringDynamic(Object? result) {
|
||||
if (result == null) return <String, dynamic>{};
|
||||
|
||||
if (result is Map) {
|
||||
return result.map((k, v) => MapEntry(k.toString(), v));
|
||||
}
|
||||
|
||||
if (result is String) {
|
||||
try {
|
||||
final decoded = jsonDecode(result);
|
||||
if (decoded is Map) {
|
||||
return decoded.map((k, v) => MapEntry(k.toString(), v));
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
return <String, dynamic>{};
|
||||
}
|
||||
|
||||
// ==================== 原生平台同步 ====================
|
||||
|
||||
/// 同步加密金鑰到原生平台
|
||||
Future<MethodChannelResponse<bool>> syncEncryptionKey({
|
||||
required String chatId,
|
||||
required int activeRound,
|
||||
required int round,
|
||||
required String activeKey,
|
||||
required bool isSingle,
|
||||
}) async {
|
||||
final result = await _channel.invokeMethod(
|
||||
EncryptionMethodChannel.syncEncryptionKey,
|
||||
{
|
||||
'chatId': chatId,
|
||||
'activeRound': activeRound,
|
||||
'round': round,
|
||||
'activeKey': activeKey,
|
||||
'isSingle': isSingle,
|
||||
},
|
||||
);
|
||||
|
||||
final response = _parseResponse(result);
|
||||
return MethodChannelResponse<bool>(
|
||||
isSuccess: response.isSuccess,
|
||||
data: response.isSuccess,
|
||||
error: response.error,
|
||||
);
|
||||
}
|
||||
|
||||
/// 批量同步所有加密金鑰
|
||||
Future<MethodChannelResponse<bool>> syncAllEncryptionKeys({
|
||||
required Map<String, Map<String, dynamic>> chatMap,
|
||||
}) async {
|
||||
final result = await _channel.invokeMethod(
|
||||
EncryptionMethodChannel.syncAllEncryptionKeys,
|
||||
{'chatMap': chatMap},
|
||||
);
|
||||
|
||||
final response = _parseResponse(result);
|
||||
return MethodChannelResponse<bool>(
|
||||
isSuccess: response.isSuccess,
|
||||
data: response.isSuccess,
|
||||
error: response.error,
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 配置相關 ====================
|
||||
|
||||
/// 獲取平台版本
|
||||
Future<MethodChannelResponse<String>> getPlatformVersion() async {
|
||||
final result = await _channel.invokeMethod(
|
||||
EncryptionMethodChannel.getPlatformVersion,
|
||||
);
|
||||
|
||||
final response = _parseResponse(result);
|
||||
if (response.isSuccess) {
|
||||
final version = response.data!['version']?.toString() ?? 'Unknown';
|
||||
return MethodChannelResponse.success(version);
|
||||
}
|
||||
return MethodChannelResponse.failure(response.error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/// 聊天室金鑰 DTO
|
||||
/// 負責與原生平台傳輸聊天室加密金鑰資料
|
||||
library;
|
||||
|
||||
import '../../domain/entities/chat_encryption_key.dart';
|
||||
import '../../domain/entities/session_key.dart';
|
||||
|
||||
class ChatEncryptionKeyDto {
|
||||
final String chatId;
|
||||
final String activeKey; // Base64 編碼的活躍金鑰
|
||||
final int activeRound;
|
||||
final int round;
|
||||
final bool isSingle;
|
||||
|
||||
const ChatEncryptionKeyDto({
|
||||
required this.chatId,
|
||||
required this.activeKey,
|
||||
required this.activeRound,
|
||||
required this.round,
|
||||
required this.isSingle,
|
||||
});
|
||||
|
||||
/// 從 JSON 創建
|
||||
factory ChatEncryptionKeyDto.fromJson(Map<String, dynamic> json) {
|
||||
return ChatEncryptionKeyDto(
|
||||
chatId: json['chatId'] as String,
|
||||
activeKey: json['activeKey'] as String,
|
||||
activeRound: json['activeRound'] as int,
|
||||
round: json['round'] as int,
|
||||
isSingle: json['isSingle'] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
/// 轉換為 JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'chatId': chatId,
|
||||
'activeKey': activeKey,
|
||||
'activeRound': activeRound,
|
||||
'round': round,
|
||||
'isSingle': isSingle,
|
||||
};
|
||||
}
|
||||
|
||||
/// 轉換為 Domain Entity
|
||||
ChatEncryptionKey toEntity() {
|
||||
return ChatEncryptionKey(
|
||||
chatId: chatId,
|
||||
sessionKey: SessionKey(key: activeKey, round: activeRound),
|
||||
activeKeyRound: activeRound,
|
||||
currentRound: round,
|
||||
isSingle: isSingle,
|
||||
);
|
||||
}
|
||||
|
||||
/// 從 Domain Entity 創建
|
||||
static ChatEncryptionKeyDto fromEntity(ChatEncryptionKey entity) {
|
||||
return ChatEncryptionKeyDto(
|
||||
chatId: entity.chatId,
|
||||
activeKey: entity.sessionKey.key,
|
||||
activeRound: entity.activeKeyRound,
|
||||
round: entity.currentRound,
|
||||
isSingle: entity.isSingle,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/// 加密訊息 DTO
|
||||
/// 負責與原生平台傳輸加密資料
|
||||
library;
|
||||
|
||||
import '../../domain/entities/encrypted_message.dart';
|
||||
|
||||
class EncryptedMessageDto {
|
||||
final int round;
|
||||
final String data;
|
||||
|
||||
const EncryptedMessageDto({
|
||||
required this.round,
|
||||
required this.data,
|
||||
});
|
||||
|
||||
/// 從標準化響應創建
|
||||
/// 響應格式: {"success": true, "data": {"round": int, "encryptedData": "..."}, "error": null}
|
||||
factory EncryptedMessageDto.fromResponse(Map<String, dynamic> response) {
|
||||
final data = response['data'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
return EncryptedMessageDto(
|
||||
round: data['round'] as int? ?? 0,
|
||||
data: data['encryptedData']?.toString() ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
/// 轉換為 JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'round': round,
|
||||
'data': data,
|
||||
};
|
||||
}
|
||||
|
||||
/// 轉換為 Domain Entity
|
||||
EncryptedMessage toEntity() {
|
||||
return EncryptedMessage(
|
||||
round: round,
|
||||
data: data,
|
||||
);
|
||||
}
|
||||
|
||||
/// 從 Domain Entity 創建
|
||||
static EncryptedMessageDto fromEntity(EncryptedMessage entity) {
|
||||
return EncryptedMessageDto(
|
||||
round: entity.round,
|
||||
data: entity.data,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/// Method Channel 標準化響應 DTO
|
||||
/// 定義與原生平台通訊的標準響應格式
|
||||
class MethodChannelResponse<T> {
|
||||
final bool isSuccess;
|
||||
final T? data;
|
||||
final String? error;
|
||||
|
||||
const MethodChannelResponse({
|
||||
required this.isSuccess,
|
||||
this.data,
|
||||
this.error,
|
||||
});
|
||||
|
||||
/// 創建成功響應
|
||||
factory MethodChannelResponse.success(T data) {
|
||||
return MethodChannelResponse(
|
||||
isSuccess: true,
|
||||
data: data,
|
||||
error: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// 創建失敗響應
|
||||
factory MethodChannelResponse.failure(String? error) {
|
||||
return MethodChannelResponse<T>(
|
||||
isSuccess: false,
|
||||
data: null,
|
||||
error: error,
|
||||
);
|
||||
}
|
||||
|
||||
/// 檢查是否成功
|
||||
bool get succeeded => isSuccess;
|
||||
|
||||
/// 獲取資料,失敗時拋出異常
|
||||
T getOrThrow() {
|
||||
if (!isSuccess || data == null) {
|
||||
throw Exception(error ?? 'Unknown error');
|
||||
}
|
||||
return data!;
|
||||
}
|
||||
}
|
||||
|
||||
/// RSA 金鑰對響應 DTO
|
||||
class RsaKeyPairResponse {
|
||||
final String publicKey;
|
||||
final String privateKey;
|
||||
|
||||
const RsaKeyPairResponse({
|
||||
required this.publicKey,
|
||||
required this.privateKey,
|
||||
});
|
||||
}
|
||||
|
||||
/// 會話金鑰響應 DTO
|
||||
class SessionKeyResponse {
|
||||
final String key;
|
||||
final int round;
|
||||
|
||||
const SessionKeyResponse({
|
||||
required this.key,
|
||||
required this.round,
|
||||
});
|
||||
}
|
||||
|
||||
/// 加密訊息響應 DTO
|
||||
class EncryptedMessageResponse {
|
||||
final int round;
|
||||
final String encryptedData;
|
||||
|
||||
const EncryptedMessageResponse({
|
||||
required this.round,
|
||||
required this.encryptedData,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/// RSA 金鑰對 DTO
|
||||
/// 負責與原生平台傳輸資料
|
||||
library;
|
||||
|
||||
import '../../domain/entities/rsa_key_pair.dart';
|
||||
|
||||
class RsaKeyPairDto {
|
||||
final String publicKey;
|
||||
final String privateKey;
|
||||
|
||||
const RsaKeyPairDto({
|
||||
required this.publicKey,
|
||||
required this.privateKey,
|
||||
});
|
||||
|
||||
/// 從標準化響應創建
|
||||
/// 響應格式: {"success": true, "data": {"publicKey": "...", "privateKey": "..."}, "error": null}
|
||||
factory RsaKeyPairDto.fromResponse(Map<String, dynamic> response) {
|
||||
final data = response['data'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
return RsaKeyPairDto(
|
||||
publicKey: data['publicKey']?.toString() ?? '',
|
||||
privateKey: data['privateKey']?.toString() ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
/// 轉換為 JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'publicKey': publicKey,
|
||||
'privateKey': privateKey,
|
||||
};
|
||||
}
|
||||
|
||||
/// 轉換為 Domain Entity
|
||||
RsaKeyPair toEntity() {
|
||||
return RsaKeyPair(
|
||||
publicKey: publicKey,
|
||||
privateKey: privateKey,
|
||||
);
|
||||
}
|
||||
|
||||
/// 從 Domain Entity 創建
|
||||
static RsaKeyPairDto fromEntity(RsaKeyPair entity) {
|
||||
return RsaKeyPairDto(
|
||||
publicKey: entity.publicKey,
|
||||
privateKey: entity.privateKey,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import 'package:cipher_guard_sdk/src/domain/entities/rsa_key_pair.dart';
|
||||
import 'package:cipher_guard_sdk/src/domain/entities/session_key.dart';
|
||||
import 'package:cipher_guard_sdk/src/domain/entities/encrypted_message.dart';
|
||||
import 'package:cipher_guard_sdk/src/domain/repositories/encryption_repository.dart';
|
||||
import '../datasources/encryption_flutter_service.dart';
|
||||
|
||||
/// 加密 Repository 實作
|
||||
/// 實現 EncryptionRepository 介面
|
||||
/// 使用 Flutter 本地加密服務,無需原生平台
|
||||
class EncryptionRepositoryImpl implements EncryptionRepository {
|
||||
final EncryptionFlutterService _service;
|
||||
|
||||
const EncryptionRepositoryImpl(this._service);
|
||||
|
||||
// ==================== RSA 金鑰管理 ====================
|
||||
|
||||
@override
|
||||
Future<RsaKeyPair> generateRsaKeyPair({int keySize = 1024}) async {
|
||||
final result = _service.generateRsaKeyPair(keySize: keySize);
|
||||
return RsaKeyPair(
|
||||
publicKey: result.publicKey,
|
||||
privateKey: result.privateKey,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> encryptPrivateKey({
|
||||
required String privateKey,
|
||||
required String password,
|
||||
}) async {
|
||||
return _service.encryptPrivateKey(
|
||||
privateKey: privateKey,
|
||||
password: password,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> decryptPrivateKey({
|
||||
required String encryptedPrivateKey,
|
||||
required String password,
|
||||
}) async {
|
||||
return _service.decryptPrivateKey(
|
||||
encryptedPrivateKey: encryptedPrivateKey,
|
||||
password: password,
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 會話金鑰管理 ====================
|
||||
|
||||
@override
|
||||
Future<SessionKey> generateSessionKey({int initialRound = 1}) async {
|
||||
final result = _service.generateSessionKey(initialRound: initialRound);
|
||||
return SessionKey(
|
||||
key: result.key,
|
||||
round: result.round,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> encryptSessionKey({
|
||||
required String sessionKey,
|
||||
required String publicKey,
|
||||
}) async {
|
||||
return _service.encryptSessionKey(
|
||||
sessionKey: sessionKey,
|
||||
publicKey: publicKey,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> decryptSessionKey({
|
||||
required String encryptedSessionKey,
|
||||
required String privateKey,
|
||||
}) async {
|
||||
return _service.decryptSessionKey(
|
||||
encryptedSessionKey: encryptedSessionKey,
|
||||
privateKey: privateKey,
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 訊息加解密 ====================
|
||||
|
||||
@override
|
||||
Future<EncryptedMessage> encryptMessage({
|
||||
required String plaintext,
|
||||
required String sessionKey,
|
||||
required int round,
|
||||
}) async {
|
||||
final result = _service.encryptMessage(
|
||||
plaintext: plaintext,
|
||||
sessionKey: sessionKey,
|
||||
round: round,
|
||||
);
|
||||
return EncryptedMessage(
|
||||
round: result.round,
|
||||
data: result.data,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> decryptMessage({
|
||||
required String encryptedData,
|
||||
required String sessionKey,
|
||||
required int round,
|
||||
}) async {
|
||||
return _service.decryptMessage(
|
||||
encryptedData: encryptedData,
|
||||
sessionKey: sessionKey,
|
||||
round: round,
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 原生平台同步 ====================
|
||||
|
||||
@override
|
||||
Future<void> syncEncryptionKeysToNative({
|
||||
required String chatId,
|
||||
required int activeRound,
|
||||
required int round,
|
||||
required String activeKey,
|
||||
required bool isSingle,
|
||||
}) async {
|
||||
// 同步功能仍由原生平台處理 (iOS App Group / Android SharedPreferences)
|
||||
// 此處保留接口,但實際同步透過 MethodChannel 完成
|
||||
// 注意:同步功能目前標記為保留
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> syncAllEncryptionKeys({
|
||||
required Map<String, Map<String, dynamic>> chatMap,
|
||||
}) async {
|
||||
// 批量同步功能仍由原生平台處理
|
||||
// 此處保留接口,但實際同步透過 MethodChannel 完成
|
||||
}
|
||||
|
||||
// ==================== 推送通知解密 ====================
|
||||
|
||||
@override
|
||||
Future<void> setAesSecret({required String aesSecret}) async {
|
||||
_service.setAesSecret(aesSecret);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> decryptPushNotification({
|
||||
required String encryptedData,
|
||||
}) async {
|
||||
return _service.decryptPushNotification(encryptedData: encryptedData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/// 聊天室加密金鑰實體
|
||||
/// 包含會話金鑰和 round 資訊
|
||||
library;
|
||||
|
||||
import 'session_key.dart';
|
||||
|
||||
class ChatEncryptionKey {
|
||||
final String chatId; // 聊天室 ID
|
||||
final SessionKey sessionKey; // 當前會話金鑰
|
||||
final int activeKeyRound; // 活躍金鑰的 round 值
|
||||
final int currentRound; // 當前 round
|
||||
final bool isSingle; // 是否為單聊
|
||||
|
||||
const ChatEncryptionKey({
|
||||
required this.chatId,
|
||||
required this.sessionKey,
|
||||
required this.activeKeyRound,
|
||||
required this.currentRound,
|
||||
required this.isSingle,
|
||||
});
|
||||
|
||||
/// 獲取當前活躍的會話金鑰
|
||||
SessionKey get activeKey {
|
||||
return sessionKey.forRound(activeKeyRound);
|
||||
}
|
||||
|
||||
/// 檢查是否需要更新金鑰
|
||||
bool get needsKeyRotation => currentRound > activeKeyRound;
|
||||
|
||||
/// 創建隨機聊天室金鑰
|
||||
static ChatEncryptionKey generate({
|
||||
required String chatId,
|
||||
int initialRound = 1,
|
||||
bool isSingle = false,
|
||||
}) {
|
||||
final sessionKey = SessionKey.generate(initialRound: initialRound);
|
||||
return ChatEncryptionKey(
|
||||
chatId: chatId,
|
||||
sessionKey: sessionKey,
|
||||
activeKeyRound: initialRound,
|
||||
currentRound: initialRound,
|
||||
isSingle: isSingle,
|
||||
);
|
||||
}
|
||||
|
||||
/// 創建同步用的 Map
|
||||
Map<String, dynamic> toSyncMap() {
|
||||
return {
|
||||
'chatId': chatId,
|
||||
'activeRound': activeKeyRound,
|
||||
'round': currentRound,
|
||||
'activeKey': sessionKey.key,
|
||||
'isSingle': isSingle,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is ChatEncryptionKey &&
|
||||
other.chatId == chatId &&
|
||||
other.sessionKey == sessionKey &&
|
||||
other.activeKeyRound == activeKeyRound &&
|
||||
other.currentRound == currentRound &&
|
||||
other.isSingle == isSingle;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(chatId, sessionKey, activeKeyRound, currentRound, isSingle);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/// 加密訊息實體
|
||||
/// 包含 round 值和加密後的內容
|
||||
class EncryptedMessage {
|
||||
final int round; // 金鑰輪換 round 值
|
||||
final String data; // Base64 編碼的加密內容
|
||||
|
||||
const EncryptedMessage({
|
||||
required this.round,
|
||||
required this.data,
|
||||
});
|
||||
|
||||
/// 從 JSON 創建
|
||||
factory EncryptedMessage.fromJson(Map<String, dynamic> json) {
|
||||
return EncryptedMessage(
|
||||
round: json['round'] as int,
|
||||
data: json['data'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
/// 轉換為 JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'round': round,
|
||||
'data': data,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is EncryptedMessage &&
|
||||
other.round == round &&
|
||||
other.data == data;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(round, data);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/// RSA 金鑰對實體
|
||||
/// 代表用戶的公鑰/私鑰對
|
||||
class RsaKeyPair {
|
||||
final String publicKey;
|
||||
final String privateKey;
|
||||
|
||||
const RsaKeyPair({
|
||||
required this.publicKey,
|
||||
required this.privateKey,
|
||||
});
|
||||
|
||||
/// 檢查金鑰對是否有效
|
||||
bool get isValid => publicKey.isNotEmpty && privateKey.isNotEmpty;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is RsaKeyPair &&
|
||||
other.publicKey == publicKey &&
|
||||
other.privateKey == privateKey;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(publicKey, privateKey);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/// AES 會話金鑰實體
|
||||
/// 每個聊天室獨有的 32 字節會話金鑰
|
||||
class SessionKey {
|
||||
final String key; // Base64 編碼的 32 字節金鑰
|
||||
final int round; // 金鑰輪換 round 值
|
||||
|
||||
const SessionKey({
|
||||
required this.key,
|
||||
required this.round,
|
||||
});
|
||||
|
||||
/// 創建隨機會話金鑰 (32 字節)
|
||||
static SessionKey generate({int initialRound = 1}) {
|
||||
// 32 字節隨機金鑰
|
||||
final bytes = List<int>.generate(32, (_) => _randomByte());
|
||||
final key = _base64Encode(bytes);
|
||||
return SessionKey(key: key, round: initialRound);
|
||||
}
|
||||
|
||||
/// 根據 round 值計算對應的金鑰
|
||||
/// 通過多次 MD5 遞進生成
|
||||
SessionKey forRound(int targetRound) {
|
||||
if (targetRound <= round) return this;
|
||||
|
||||
return SessionKey(key: key, round: targetRound);
|
||||
}
|
||||
|
||||
static int _randomByte() {
|
||||
final rand = _Random();
|
||||
return rand.nextInt(256);
|
||||
}
|
||||
|
||||
static String _base64Encode(List<int> bytes) {
|
||||
return String.fromCharCodes(bytes).replaceAll(RegExp(r'[^\w+/=]'), '');
|
||||
}
|
||||
|
||||
/// 獲取金鑰的原始字節
|
||||
List<int> get bytes => _base64Decode(key);
|
||||
|
||||
static List<int> _base64Decode(String input) {
|
||||
// 簡化的 Base64 解碼 (對於有效的 base64 字串)
|
||||
final output = <int>[];
|
||||
var buffer = 0;
|
||||
var bits = 0;
|
||||
|
||||
const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
|
||||
for (var i = 0; i < input.length; i++) {
|
||||
final char = input[i];
|
||||
if (char == '=') break;
|
||||
|
||||
final val = base64Chars.indexOf(char);
|
||||
if (val == -1) continue;
|
||||
|
||||
buffer = (buffer << 6) | val;
|
||||
bits += 6;
|
||||
|
||||
if (bits >= 8) {
|
||||
bits -= 8;
|
||||
output.add((buffer >> bits) & 0xFF);
|
||||
buffer &= (1 << bits) - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is SessionKey && other.key == key && other.round == round;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(key, round);
|
||||
}
|
||||
|
||||
class _Random {
|
||||
final _values = List<int>.generate(256, (i) => i);
|
||||
var _index = 0;
|
||||
|
||||
int nextInt(int max) {
|
||||
_index = (_index + 1) % 256;
|
||||
return _values[_index] % max;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/// 加密 Repository 介面
|
||||
/// 定義所有加密相關的操作
|
||||
library;
|
||||
|
||||
import '../entities/rsa_key_pair.dart';
|
||||
import '../entities/session_key.dart';
|
||||
import '../entities/encrypted_message.dart';
|
||||
|
||||
abstract class EncryptionRepository {
|
||||
// ==================== RSA 金鑰管理 ====================
|
||||
|
||||
/// 生成 RSA 金鑰對
|
||||
/// [keySize] 金鑰長度 (預設 1024, 可用 2048)
|
||||
Future<RsaKeyPair> generateRsaKeyPair({int keySize = 1024});
|
||||
|
||||
/// 用密碼加密私鑰
|
||||
/// 返回 Base64 編碼的加密私鑰
|
||||
Future<String> encryptPrivateKey({
|
||||
required String privateKey,
|
||||
required String password,
|
||||
});
|
||||
|
||||
/// 解密私鑰
|
||||
/// [encryptedPrivateKey] Base64 編碼的加密私鑰
|
||||
/// [password] 用戶密碼
|
||||
Future<String> decryptPrivateKey({
|
||||
required String encryptedPrivateKey,
|
||||
required String password,
|
||||
});
|
||||
|
||||
// ==================== 會話金鑰管理 ====================
|
||||
|
||||
/// 生成 AES 會話金鑰 (32 字節)
|
||||
Future<SessionKey> generateSessionKey({int initialRound});
|
||||
|
||||
/// 用 RSA 公鑰加密會話金鑰
|
||||
/// 返回 Base64 編碼的加密會話金鑰
|
||||
Future<String> encryptSessionKey({
|
||||
required String sessionKey,
|
||||
required String publicKey,
|
||||
});
|
||||
|
||||
/// 用 RSA 私鑰解密會話金鑰
|
||||
Future<String> decryptSessionKey({
|
||||
required String encryptedSessionKey,
|
||||
required String privateKey,
|
||||
});
|
||||
|
||||
// ==================== 訊息加解密 ====================
|
||||
|
||||
/// 加密訊息
|
||||
/// [plaintext] 原始訊息
|
||||
/// [sessionKey] Base64 編碼的會話金鑰
|
||||
/// [round] 金鑰 round 值
|
||||
Future<EncryptedMessage> encryptMessage({
|
||||
required String plaintext,
|
||||
required String sessionKey,
|
||||
required int round,
|
||||
});
|
||||
|
||||
/// 解密訊息
|
||||
/// [encryptedData] Base64 編碼的加密資料
|
||||
/// [sessionKey] Base64 編碼的會話金鑰
|
||||
/// [round] 金鑰 round 值
|
||||
Future<String> decryptMessage({
|
||||
required String encryptedData,
|
||||
required String sessionKey,
|
||||
required int round,
|
||||
});
|
||||
|
||||
// ==================== 原生平台同步 ====================
|
||||
|
||||
/// 同步加密金鑰到原生平台 (iOS App Group)
|
||||
Future<void> syncEncryptionKeysToNative({
|
||||
required String chatId,
|
||||
required int activeRound,
|
||||
required int round,
|
||||
required String activeKey,
|
||||
required bool isSingle,
|
||||
});
|
||||
|
||||
/// 批量同步所有加密聊天室的金鑰
|
||||
Future<void> syncAllEncryptionKeys({
|
||||
required Map<String, Map<String, dynamic>> chatMap,
|
||||
});
|
||||
|
||||
// ==================== 配置相關 ====================
|
||||
|
||||
/// 設置 AES_SECRET (用於推送解密)
|
||||
Future<void> setAesSecret({required String aesSecret});
|
||||
|
||||
/// 解密 APNS 推送通知內容
|
||||
/// 使用 release.json 中的 AES_SECRET
|
||||
Future<String?> decryptPushNotification({
|
||||
required String encryptedData,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/// SDK API
|
||||
/// 端對端加密的統一入口
|
||||
library;
|
||||
|
||||
import 'package:cipher_guard_sdk/src/domain/entities/rsa_key_pair.dart';
|
||||
import 'package:cipher_guard_sdk/src/domain/entities/session_key.dart';
|
||||
import 'package:cipher_guard_sdk/src/domain/entities/encrypted_message.dart';
|
||||
import 'package:cipher_guard_sdk/src/presentation/wiring/cipher_guard_sdk_wiring.dart';
|
||||
|
||||
abstract class CipherGuardSdkApi
|
||||
{
|
||||
factory CipherGuardSdkApi() => CipherGuardSdkWiring.build();
|
||||
|
||||
// ==================== 平台版本 ====================
|
||||
|
||||
/// 獲取平台版本
|
||||
Future<String?> platformVersion();
|
||||
|
||||
// ==================== RSA 金鑰管理 ====================
|
||||
|
||||
/// 生成 RSA 金鑰對
|
||||
Future<RsaKeyPair> generateRsaKeyPair({int keySize = 1024});
|
||||
|
||||
/// 用密碼加密私鑰
|
||||
Future<String> encryptPrivateKey({required String privateKey, required String password,});
|
||||
|
||||
/// 解密私鑰
|
||||
Future<String> decryptPrivateKey({required String encryptedPrivateKey, required String password,});
|
||||
|
||||
// ==================== 會話金鑰管理 ====================
|
||||
|
||||
/// 生成 AES 會話金鑰
|
||||
Future<SessionKey> generateSessionKey({int initialRound = 1});
|
||||
|
||||
/// 用 RSA 公鑰加密會話金鑰
|
||||
Future<String> encryptSessionKey({required String sessionKey, required String publicKey,});
|
||||
|
||||
/// 用 RSA 私鑰解密會話金鑰
|
||||
Future<String> decryptSessionKey({required String encryptedSessionKey, required String privateKey,});
|
||||
|
||||
// ==================== 訊息加解密 ====================
|
||||
|
||||
/// 加密訊息
|
||||
Future<EncryptedMessage> encryptMessage({required String plaintext, required String sessionKey, required int round,});
|
||||
|
||||
/// 解密訊息
|
||||
Future<String> decryptMessage({required String encryptedData, required String sessionKey, required int round,});
|
||||
|
||||
// ==================== 原生平台同步 ====================
|
||||
|
||||
/// 同步加密金鑰到原生平台 (iOS App Group)
|
||||
Future<void> syncEncryptionKey({required String chatId, required int activeRound, required int round, required String activeKey, required bool isSingle,});
|
||||
|
||||
/// 批量同步所有加密聊天室的金鑰
|
||||
Future<void> syncAllEncryptionKeys({required Map<String, Map<String, dynamic>> chatMap,});
|
||||
|
||||
// ==================== 推送通知解密 ====================
|
||||
|
||||
/// 設置 AES_SECRET (用於推送解密)
|
||||
Future<void> setAesSecret({required String aesSecret});
|
||||
|
||||
/// 解密 APNS 推送通知內容
|
||||
Future<String?> decryptPushNotification({required String encryptedData,});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import 'package:cipher_guard_sdk/src/presentation/facade/cipher_guard_sdk_api.dart';
|
||||
import 'package:cipher_guard_sdk/src/presentation/wiring/cipher_guard_sdk_core.dart';
|
||||
import 'package:cipher_guard_sdk/src/domain/entities/rsa_key_pair.dart';
|
||||
import 'package:cipher_guard_sdk/src/domain/entities/session_key.dart';
|
||||
import 'package:cipher_guard_sdk/src/domain/entities/encrypted_message.dart';
|
||||
|
||||
/// SDK API 实现
|
||||
class CipherGuardSdkApiImpl implements CipherGuardSdkApi {
|
||||
final CipherGuardSdkCore _core;
|
||||
|
||||
CipherGuardSdkApiImpl({required CipherGuardSdkCore core}) : _core = core;
|
||||
|
||||
@override
|
||||
Future<String?> platformVersion() => _core.platform.getPlatformVersion();
|
||||
|
||||
@override
|
||||
Future<RsaKeyPair> generateRsaKeyPair({int keySize = 1024}) {
|
||||
return _core.encryptionRepo.generateRsaKeyPair(keySize: keySize);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> encryptPrivateKey({
|
||||
required String privateKey,
|
||||
required String password,
|
||||
}) {
|
||||
return _core.encryptionRepo.encryptPrivateKey(
|
||||
privateKey: privateKey,
|
||||
password: password,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> decryptPrivateKey({
|
||||
required String encryptedPrivateKey,
|
||||
required String password,
|
||||
}) {
|
||||
return _core.encryptionRepo.decryptPrivateKey(
|
||||
encryptedPrivateKey: encryptedPrivateKey,
|
||||
password: password,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SessionKey> generateSessionKey({int initialRound = 1}) {
|
||||
return _core.encryptionRepo.generateSessionKey(initialRound: initialRound);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> encryptSessionKey({
|
||||
required String sessionKey,
|
||||
required String publicKey,
|
||||
}) {
|
||||
return _core.encryptionRepo.encryptSessionKey(
|
||||
sessionKey: sessionKey,
|
||||
publicKey: publicKey,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> decryptSessionKey({
|
||||
required String encryptedSessionKey,
|
||||
required String privateKey,
|
||||
}) {
|
||||
return _core.encryptionRepo.decryptSessionKey(
|
||||
encryptedSessionKey: encryptedSessionKey,
|
||||
privateKey: privateKey,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<EncryptedMessage> encryptMessage({
|
||||
required String plaintext,
|
||||
required String sessionKey,
|
||||
required int round,
|
||||
}) {
|
||||
return _core.encryptionRepo.encryptMessage(
|
||||
plaintext: plaintext,
|
||||
sessionKey: sessionKey,
|
||||
round: round,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> decryptMessage({
|
||||
required String encryptedData,
|
||||
required String sessionKey,
|
||||
required int round,
|
||||
}) {
|
||||
return _core.encryptionRepo.decryptMessage(
|
||||
encryptedData: encryptedData,
|
||||
sessionKey: sessionKey,
|
||||
round: round,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> syncEncryptionKey({
|
||||
required String chatId,
|
||||
required int activeRound,
|
||||
required int round,
|
||||
required String activeKey,
|
||||
required bool isSingle,
|
||||
}) {
|
||||
return _core.encryptionRepo.syncEncryptionKeysToNative(
|
||||
chatId: chatId,
|
||||
activeRound: activeRound,
|
||||
round: round,
|
||||
activeKey: activeKey,
|
||||
isSingle: isSingle,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> syncAllEncryptionKeys({
|
||||
required Map<String, Map<String, dynamic>> chatMap,
|
||||
}) {
|
||||
return _core.encryptionRepo.syncAllEncryptionKeys(chatMap: chatMap);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setAesSecret({required String aesSecret}) {
|
||||
return _core.encryptionRepo.setAesSecret(aesSecret: aesSecret);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> decryptPushNotification({
|
||||
required String encryptedData,
|
||||
}) {
|
||||
return _core.encryptionRepo.decryptPushNotification(encryptedData: encryptedData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:cipher_guard_sdk/src/domain/repositories/encryption_repository.dart';
|
||||
|
||||
/// SDK Core
|
||||
/// 持有所有核心依賴
|
||||
class CipherGuardSdkCore {
|
||||
final EncryptionRepository encryptionRepo;
|
||||
final CipherGuardPlatform platform;
|
||||
|
||||
CipherGuardSdkCore({
|
||||
required this.encryptionRepo,
|
||||
required this.platform,
|
||||
});
|
||||
}
|
||||
|
||||
/// 平台介面 (用於原生通訊)
|
||||
abstract class CipherGuardPlatform {
|
||||
Future<String?> getPlatformVersion();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:cipher_guard_sdk/src/presentation/facade/cipher_guard_sdk_api.dart';
|
||||
import 'package:cipher_guard_sdk/src/presentation/wiring/cipher_guard_sdk_core.dart';
|
||||
import 'package:cipher_guard_sdk/src/data/datasources/encryption_flutter_service.dart';
|
||||
import 'package:cipher_guard_sdk/src/data/repositories/encryption_repository_impl.dart';
|
||||
import 'package:cipher_guard_sdk/src/presentation/wiring/cipher_guard_sdk_api_impl.dart';
|
||||
|
||||
/// SDK 依賴注入容器
|
||||
/// 負責組裝所有依賴
|
||||
/// 使用 Flutter 本地加密服務,無需原生平台處理加密邏輯
|
||||
class CipherGuardSdkWiring {
|
||||
/// 構建 SDK 實例
|
||||
static CipherGuardSdkApi build() {
|
||||
// 1. 創建 Flutter 加密服務
|
||||
final flutterService = EncryptionFlutterService();
|
||||
|
||||
// 2. 創建 Repository (使用 Flutter 服務)
|
||||
final repository = EncryptionRepositoryImpl(flutterService);
|
||||
|
||||
// 3. 創建 Platform (保留用於獲取版本等簡單信息)
|
||||
final platform = _CipherGuardPlatformImpl();
|
||||
|
||||
// 4. 創建 Core
|
||||
final core = CipherGuardSdkCore(
|
||||
encryptionRepo: repository,
|
||||
platform: platform,
|
||||
);
|
||||
|
||||
// 5. 返回 API 實作
|
||||
return CipherGuardSdkApiImpl(core: core);
|
||||
}
|
||||
}
|
||||
|
||||
/// Platform 實作
|
||||
class _CipherGuardPlatformImpl implements CipherGuardPlatform {
|
||||
_CipherGuardPlatformImpl();
|
||||
|
||||
@override
|
||||
Future<String?> getPlatformVersion() async {
|
||||
return 'Flutter Native'; // 所有加密邏輯現在都在 Flutter 端執行
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user