|
@@ -1,54 +1,329 @@
|
|
|
|
|
|
|
|
import 'dart:convert';
|
|
import 'dart:convert';
|
|
|
import 'dart:typed_data';
|
|
import 'dart:typed_data';
|
|
|
-import 'package:encrypt/encrypt.dart';
|
|
|
|
|
|
|
+import 'package:encrypt/encrypt.dart' as encrypt;
|
|
|
|
|
+import 'package:pointycastle/export.dart';
|
|
|
|
|
+import '../utils/hexUtils.dart';
|
|
|
|
|
+import 'cryptData.dart';
|
|
|
|
|
|
|
|
|
|
|
|
|
-class CryptData {
|
|
|
|
|
- final Uint8List _byte;
|
|
|
|
|
|
|
|
|
|
- CryptData(Uint8List byte) : _byte = byte;
|
|
|
|
|
- CryptData.fromUtf8(String base64data) : _byte = base64Decode(base64data);
|
|
|
|
|
- CryptData.fromBase64(String base64data) : _byte = base64Decode(base64data);
|
|
|
|
|
- CryptData.fromHex(String hexData) : _byte = decodeHexString(hexData);
|
|
|
|
|
-
|
|
|
|
|
- Uint8List get byte => _byte;
|
|
|
|
|
- String get base64 => base64Encode(_byte);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-class EncryptedAesCbc {
|
|
|
|
|
|
|
+// AES CBC 加密结果,存储 IV 和 加密数据
|
|
|
|
|
+class CryptCBCData {
|
|
|
final CryptData iv;
|
|
final CryptData iv;
|
|
|
final CryptData encrypted;
|
|
final CryptData encrypted;
|
|
|
|
|
+ CryptData? salt;
|
|
|
|
|
|
|
|
- EncryptedAesCbc({
|
|
|
|
|
|
|
+ CryptCBCData({
|
|
|
required this.iv,
|
|
required this.iv,
|
|
|
required this.encrypted,
|
|
required this.encrypted,
|
|
|
|
|
+ this.salt,
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- EncryptedAesCbc.fromBase64({
|
|
|
|
|
|
|
+ CryptCBCData.fromBase64({
|
|
|
required String base64iv,
|
|
required String base64iv,
|
|
|
required String base64encrypted,
|
|
required String base64encrypted,
|
|
|
- }) : iv = CryptData.fromBase64(base64iv), encrypted = CryptData.fromBase64(base64encrypted);
|
|
|
|
|
|
|
+ String? base64salt,
|
|
|
|
|
+ }) : iv = CryptData.fromBase64(base64iv),
|
|
|
|
|
+ encrypted = CryptData.fromBase64(base64encrypted),
|
|
|
|
|
+ salt = base64salt != null ? CryptData.fromBase64(base64salt) : null;
|
|
|
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-EncryptedAesCbc encryptAesCbc(String plainText, CryptData password) {
|
|
|
|
|
- final iv = IV.fromLength(16); // AES-CBC 必需的16字节IV
|
|
|
|
|
- final key = Key(password.byte);
|
|
|
|
|
- final encrypter = Encrypter(AES(key, mode: AESMode.cbc));
|
|
|
|
|
|
|
|
|
|
- final encrypted = encrypter.encrypt(plainText, iv: iv);
|
|
|
|
|
|
|
|
|
|
- return EncryptedAesCbc(iv: CryptData(iv.bytes), encrypted: CryptData(encrypted.bytes));
|
|
|
|
|
-}
|
|
|
|
|
|
|
|
|
|
|
|
+/// ------------------- AES 编解码工具类 -------------------
|
|
|
|
|
+///
|
|
|
|
|
+
|
|
|
|
|
+class AESUtils {
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ /// ---------- CBC 模式 ----------
|
|
|
|
|
+
|
|
|
|
|
+ /// 默认密钥长度(字节)
|
|
|
|
|
+ static const int _defaultKeyLength = 32; // AES-256
|
|
|
|
|
+
|
|
|
|
|
+ /// 默认 IV 长度(字节)
|
|
|
|
|
+ static const int _ivLength = 16;
|
|
|
|
|
+
|
|
|
|
|
+ /// 生成随机密钥
|
|
|
|
|
+ /// [keyLength] 密钥长度:16(AES-128), 24(AES-192), 32(AES-256)
|
|
|
|
|
+ static CryptData generateKey({int keyLength = _defaultKeyLength}) {
|
|
|
|
|
+ if (![16, 24, 32].contains(keyLength)) {
|
|
|
|
|
+ throw ArgumentError('密钥长度必须是 16、24 或 32 字节');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ final secureRandom = SecureRandom('Fortuna');
|
|
|
|
|
+ secureRandom.seed(KeyParameter(Uint8List.fromList(
|
|
|
|
|
+ List.generate(32, (_) => DateTime.now().microsecond))));
|
|
|
|
|
+
|
|
|
|
|
+ return CryptData(secureRandom.nextBytes(keyLength));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// 生成随机 IV
|
|
|
|
|
+ static CryptData generateIV() {
|
|
|
|
|
+ final secureRandom = SecureRandom('Fortuna');
|
|
|
|
|
+ secureRandom.seed(KeyParameter(Uint8List.fromList(
|
|
|
|
|
+ List.generate(32, (_) => DateTime.now().microsecond))));
|
|
|
|
|
+
|
|
|
|
|
+ return CryptData(secureRandom.nextBytes(_ivLength));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// AES 加密
|
|
|
|
|
+ /// [plainText] 明文
|
|
|
|
|
+ /// [key] 密钥(16/24/32 字节)
|
|
|
|
|
+ /// [iv] 初始化向量(16 字节),如果为空则自动生成
|
|
|
|
|
+ /// 返回包含 IV 和密文的 Map,格式为 {'iv': iv_hex, 'encrypted': encrypted_base64}
|
|
|
|
|
+ static CryptCBCData encryptCBC(String plainText, CryptData key, {CryptData? iv}) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 验证密钥长度
|
|
|
|
|
+ if (![16, 24, 32].contains(key.bytes.length)) {
|
|
|
|
|
+ throw ArgumentError('密钥长度必须是 16、24 或 32 字节');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果未提供 IV,则生成新的 IV
|
|
|
|
|
+ final usedIv = iv ?? generateIV();
|
|
|
|
|
+
|
|
|
|
|
+ // 验证 IV 长度
|
|
|
|
|
+ if (usedIv.bytes.length != _ivLength) {
|
|
|
|
|
+ throw ArgumentError('IV 长度必须是 16 字节');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 创建加密器
|
|
|
|
|
+ final encrypter = encrypt.Encrypter(
|
|
|
|
|
+ encrypt.AES(
|
|
|
|
|
+ encrypt.Key(key.bytes),
|
|
|
|
|
+ mode: encrypt.AESMode.cbc,
|
|
|
|
|
+ padding: 'PKCS7',
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 执行加密
|
|
|
|
|
+ final encrypted = encrypter.encrypt(plainText, iv: encrypt.IV(usedIv.bytes));
|
|
|
|
|
+
|
|
|
|
|
+ // 返回 IV 和密文(IV用十六进制,密文用base64)
|
|
|
|
|
+ return CryptCBCData(
|
|
|
|
|
+ iv: CryptData(usedIv.bytes),
|
|
|
|
|
+ encrypted: CryptData(encrypted.bytes),
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ throw Exception('AES encrypt failed: $e');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// AES 解密
|
|
|
|
|
+ /// [encryptedText] 密文(base64 格式)
|
|
|
|
|
+ /// [key] 密钥(16/24/32 字节)
|
|
|
|
|
+ /// [iv] 初始化向量(16 字节)
|
|
|
|
|
+ /// 返回解密后的明文字符串
|
|
|
|
|
+ // static String decryptCBC(String encryptedText, Uint8List key, Uint8List iv) {
|
|
|
|
|
+ static String decryptCBC(CryptCBCData encrypted, CryptData key) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 验证密钥长度
|
|
|
|
|
+ if (![16, 24, 32].contains(key.bytes.length)) {
|
|
|
|
|
+ throw ArgumentError('key must be 16 or 24 or 32 bytes');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 验证 IV 长度
|
|
|
|
|
+ if (encrypted.iv.bytes.length != _ivLength) {
|
|
|
|
|
+ throw ArgumentError('IV must be 16 bytes');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 创建解密器
|
|
|
|
|
+ final encrypter = encrypt.Encrypter(
|
|
|
|
|
+ encrypt.AES(
|
|
|
|
|
+ encrypt.Key(key.bytes),
|
|
|
|
|
+ mode: encrypt.AESMode.cbc,
|
|
|
|
|
+ padding: 'PKCS7',
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 执行解密
|
|
|
|
|
+ final decrypted = encrypter.decrypt64(
|
|
|
|
|
+ encrypted.encrypted.base64,
|
|
|
|
|
+ iv: encrypt.IV(encrypted.iv.bytes),
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ return decrypted;
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ throw Exception('AES decrypt failed: $e');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ /// 简化的加密方法 - 自动处理密钥派生
|
|
|
|
|
+ /// [plainText] 明文
|
|
|
|
|
+ /// [password] 密码字符串,将用于派生密钥
|
|
|
|
|
+ /// [salt] 盐值(可选,十六进制格式)
|
|
|
|
|
+ /// 返回包含 IV、盐和密文的 CryptCBCData
|
|
|
|
|
+ static CryptCBCData encryptCBCWithPassword(
|
|
|
|
|
+ String plainText,
|
|
|
|
|
+ String password,
|
|
|
|
|
+ {String? salt}
|
|
|
|
|
+ ) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 如果没有提供盐值,则生成随机盐值(十六进制格式)
|
|
|
|
|
+ final usedSalt = salt ?? bytesToHex(generateIV().bytes, include0x: false);
|
|
|
|
|
|
|
|
-String decryptAesCbc(EncryptedAesCbc encrypted, CryptData password) {
|
|
|
|
|
- final iv = IV(encrypted.iv.byte);
|
|
|
|
|
- final key = Key(password.byte);
|
|
|
|
|
- final encrypter = Encrypter(AES(key, mode: AESMode.cbc));
|
|
|
|
|
|
|
+ // 从密码派生密钥(使用 PBKDF2)
|
|
|
|
|
+ final derivedKey = _deriveKeyFromPassword(password, usedSalt);
|
|
|
|
|
|
|
|
- final decrypted = encrypter.decrypt(Encrypted(encrypted.encrypted.byte), iv: iv);
|
|
|
|
|
|
|
+ // 执行加密
|
|
|
|
|
+ final result = encryptCBC(plainText, derivedKey);
|
|
|
|
|
+
|
|
|
|
|
+ // 将盐值添加到结果中
|
|
|
|
|
+ result.salt = CryptData.fromHex(usedSalt);
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ throw Exception('AES CBC encrypt with password failed: $e');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// 简化的解密方法 - 使用密码派生密钥
|
|
|
|
|
+ /// [encrypted] 包含 IV、盐和密文的 CryptCBCData
|
|
|
|
|
+ /// [password] 密码字符串
|
|
|
|
|
+ /// 返回解密后的明文字符串
|
|
|
|
|
+ static String decryptCBCWithPassword(
|
|
|
|
|
+ CryptCBCData encrypted,
|
|
|
|
|
+ String password,
|
|
|
|
|
+ ) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 验证盐值是否存在
|
|
|
|
|
+ if (encrypted.salt == null) {
|
|
|
|
|
+ throw ArgumentError('Salt is required for password-based decryption');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 将盐值转换为十六进制字符串
|
|
|
|
|
+ final saltHex = bytesToHex(encrypted.salt!.bytes, include0x: false);
|
|
|
|
|
+
|
|
|
|
|
+ // 从密码派生密钥
|
|
|
|
|
+ final derivedKey = _deriveKeyFromPassword(password, saltHex);
|
|
|
|
|
+
|
|
|
|
|
+ // 执行解密
|
|
|
|
|
+ return decryptCBC(encrypted, derivedKey);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ throw Exception('AES CBC decrypt with password failed: $e');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// 从密码派生密钥(使用 PBKDF2)
|
|
|
|
|
+ static CryptData _deriveKeyFromPassword(String password, String salt, {int keyLength = 32}) {
|
|
|
|
|
+ final pbkdf2 = PBKDF2KeyDerivator(HMac(SHA256Digest(), 64));
|
|
|
|
|
+ final params = Pbkdf2Parameters(
|
|
|
|
|
+ hexToBytes(salt),
|
|
|
|
|
+ 100000, // 迭代次数
|
|
|
|
|
+ keyLength,
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ pbkdf2.init(params);
|
|
|
|
|
+
|
|
|
|
|
+ final key = Uint8List(keyLength);
|
|
|
|
|
+ pbkdf2.deriveKey(Uint8List.fromList(utf8.encode(password)), 0, key, 0);
|
|
|
|
|
+
|
|
|
|
|
+ return CryptData(key);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// 验证密钥格式是否正确
|
|
|
|
|
+ static bool isValidKey(Uint8List key) {
|
|
|
|
|
+ return [16, 24, 32].contains(key.length);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// 验证 IV 格式是否正确
|
|
|
|
|
+ static bool isValidIV(Uint8List iv) {
|
|
|
|
|
+ return iv.length == _ivLength;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ /// ---------- ECB 模式 ----------
|
|
|
|
|
+ /// * ECB 模式不安全,现在服务器使用,提供该方法,但不建议广泛使用
|
|
|
|
|
+ /// *
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ /// ECB 模式加密 - 使用 CryptData 密钥
|
|
|
|
|
+ /// [plainText] 明文
|
|
|
|
|
+ /// [key] 密钥(CryptData,16/24/32 字节)
|
|
|
|
|
+ /// 返回 CryptData 格式的密文
|
|
|
|
|
+ static CryptData encryptECB(String plainText, CryptData key) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 验证密钥长度
|
|
|
|
|
+ if (![16, 24, 32].contains(key.bytes.length)) {
|
|
|
|
|
+ throw ArgumentError('key must be 16 or 24 or 32 bytes');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 创建加密器(ECB 模式不需要 IV)
|
|
|
|
|
+ final encrypter = encrypt.Encrypter(
|
|
|
|
|
+ encrypt.AES(
|
|
|
|
|
+ encrypt.Key(key.bytes),
|
|
|
|
|
+ mode: encrypt.AESMode.ecb,
|
|
|
|
|
+ padding: 'PKCS7',
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 执行加密并返回 CryptData 格式的密文
|
|
|
|
|
+ final encrypted = encrypter.encrypt(plainText);
|
|
|
|
|
+ return CryptData(encrypted.bytes);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ throw Exception('AES ECB encrypt failed: $e');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// ECB 模式解密 - 使用 CryptData 密钥
|
|
|
|
|
+ /// [encrypted] CryptData 格式的密文
|
|
|
|
|
+ /// [key] 密钥(CryptData,16/24/32 字节)
|
|
|
|
|
+ /// 返回解密后的明文字符串
|
|
|
|
|
+ static String decryptECB(CryptData encrypted, CryptData key) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 验证密钥长度
|
|
|
|
|
+ if (![16, 24, 32].contains(key.bytes.length)) {
|
|
|
|
|
+ throw ArgumentError('密钥长度必须是 16、24 或 32 字节(当前 ${key.bytes.length} 字节)');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 创建解密器(ECB 模式不需要 IV)
|
|
|
|
|
+ final encrypter = encrypt.Encrypter(
|
|
|
|
|
+ encrypt.AES(
|
|
|
|
|
+ encrypt.Key(key.bytes),
|
|
|
|
|
+ mode: encrypt.AESMode.ecb,
|
|
|
|
|
+ padding: 'PKCS7',
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 执行解密
|
|
|
|
|
+ final decrypted = encrypter.decryptBytes(encrypt.Encrypted(encrypted.bytes));
|
|
|
|
|
+ return utf8.decode(decrypted);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ throw Exception('AES ECB 解密失败: $e');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- return decrypted;
|
|
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+// 废弃
|
|
|
|
|
+// CryptCBCData encryptAesCbc(String plainText, CryptData password) {
|
|
|
|
|
+// final iv = encrypt.IV.fromLength(16); // AES-CBC 必需的16字节IV
|
|
|
|
|
+// final key = encrypt.Key(password.byte);
|
|
|
|
|
+// final encrypter = encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc));
|
|
|
|
|
+//
|
|
|
|
|
+// final encrypted = encrypter.encrypt(plainText, iv: iv);
|
|
|
|
|
+//
|
|
|
|
|
+// return CryptCBCData(iv: CryptData(iv.bytes), encrypted: CryptData(encrypted.bytes));
|
|
|
|
|
+// }
|
|
|
|
|
+//
|
|
|
|
|
+//
|
|
|
|
|
+// String decryptAesCbc(CryptCBCData encrypted, CryptData password) {
|
|
|
|
|
+// final iv = encrypt.IV(encrypted.iv.byte);
|
|
|
|
|
+// final key = encrypt.Key(password.byte);
|
|
|
|
|
+// final encrypter = encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc));
|
|
|
|
|
+//
|
|
|
|
|
+// final decrypted = encrypter.decrypt(encrypt.Encrypted(encrypted.encrypted.byte), iv: iv);
|
|
|
|
|
+//
|
|
|
|
|
+// return decrypted;
|
|
|
|
|
+// }
|
|
|
|
|
+
|
|
|
|
|
+
|