import 'dart:convert'; import 'package:archive/archive.dart'; import 'dart:math'; import 'dart:typed_data'; import 'package:analytics_sdk/config/sdk_config.dart'; import 'package:pointycastle/export.dart'; /// AES-GCM 加密工具类 /// 提供 AES-256-GCM 加密/解密、GZIP 压缩/解压功能 class AesGcmUtil { // AES-256 密钥长度 static const int keySize = 32; // GCM IV 长度(12 字节) static const int gcmIvLength = 12; // GCM Tag 长度(128 位 = 16 字节) static const int gcmTagLength = 16; // 随机数生成器 static final Random _random = Random.secure(); /// 生成唯一的密钥(Base64 字符串,无填充) /// 返回 32 字节的 AES-256 密钥的 Base64 编码 static String generateKey() { final keyBytes = Uint8List(keySize); for (int i = 0; i < keySize; i++) { keyBytes[i] = _random.nextInt(256); } return base64Encode(keyBytes) .replaceAll('=', '') .replaceAll('+', '-') .replaceAll('/', '_'); } /// 加密:Object → String(Base64) /// [keyBase64] Base64 编码的密钥 /// [data] 要加密的数据(任意对象,会被转换为 JSON) /// 返回 Base64 编码的加密数据(IV + 密文) static String encrypt(String keyBase64, dynamic data) { try { // 解码密钥 final key = _decodeKey(keyBase64); // 将数据转换为 JSON 字符串 final json = jsonEncode(data); final input = Uint8List.fromList(utf8.encode(json)); // 生成随机 IV(12 字节) final iv = Uint8List(gcmIvLength); for (int i = 0; i < gcmIvLength; i++) { iv[i] = _random.nextInt(256); } // 执行 AES-GCM 加密 final cipher = GCMBlockCipher(AESEngine()); final params = AEADParameters( KeyParameter(key), gcmTagLength * 8, // Tag 长度(位) iv, Uint8List(0), // 无附加认证数据(AAD) ); cipher.init(true, params); final cipherText = cipher.process(input); // 组合 IV + 密文 final output = Uint8List(iv.length + cipherText.length); output.setRange(0, iv.length, iv); output.setRange(iv.length, output.length, cipherText); // Base64 编码(无填充) return base64Encode(output) .replaceAll('=', '') .replaceAll('+', '-') .replaceAll('/', '_'); } catch (e) { throw Exception('Encryption failed: $e'); } } /// 加密(带 GZIP 压缩):先压缩再加密 /// [keyBase64] Base64 编码的密钥 /// [data] 要加密的数据(任意对象,会被转换为 JSON) /// 返回 Base64 编码的加密数据 static String encryptGzip(String keyBase64, dynamic data) { try { // 解码密钥 final key = _decodeKey(keyBase64); // 将数据转换为 JSON 字符串 final json = jsonEncode(data); // ① 先 GZIP 压缩并转 Base64 final gzipBase64 = gzipToBase64(json); // ② AES-GCM 加密这个 Base64 字符串 final input = Uint8List.fromList(utf8.encode(gzipBase64)); // 生成随机 IV(12 字节) final iv = Uint8List(gcmIvLength); for (int i = 0; i < gcmIvLength; i++) { iv[i] = _random.nextInt(256); } // 执行 AES-GCM 加密 final cipher = GCMBlockCipher(AESEngine()); final params = AEADParameters( KeyParameter(key), gcmTagLength * 8, // Tag 长度(位) iv, Uint8List(0), // 无附加认证数据(AAD) ); cipher.init(true, params); final cipherText = cipher.process(input); // 组合 IV + 密文 final output = Uint8List(iv.length + cipherText.length); output.setRange(0, iv.length, iv); output.setRange(iv.length, output.length, cipherText); // Base64 编码(无填充) return base64Encode(output) .replaceAll('=', '') .replaceAll('+', '-') .replaceAll('/', '_'); } catch (e) { throw Exception('Encryption failed: $e'); } } /// 解密:返回明文 JSON 字符串 /// [keyBase64] Base64 编码的密钥 /// [encryptedBase64] Base64 编码的加密数据(IV + 密文) /// 返回解密后的 JSON 字符串 static String decryptToString(String keyBase64, String encryptedBase64) { try { // 解码 Base64(处理 URL 安全格式) String normalizedBase64 = encryptedBase64.replaceAll('-', '+').replaceAll('_', '/'); while (normalizedBase64.length % 4 != 0) { normalizedBase64 += '='; } final data = base64Decode(normalizedBase64); return decryptFromCipherBytes(keyBase64, Uint8List.fromList(data)); } catch (e) { throw Exception('Decryption failed: $e'); } } /// 解密:从原始字节(IV + 密文)解密为明文字符串 /// [keyBase64] Base64 编码的密钥 /// [cipherBytes] 密文字节:前 12 字节为 IV,后续为 AES-GCM 密文 /// 返回解密后的 UTF-8 字符串 /// /// 若抛出 [InvalidCipherTextException](或 "Decryption failed: InvalidCipherTextException"), /// 常见原因:① 密钥与加密端不一致 ② 服务端密文格式不是「前 12 字节 IV + 密文」 /// (如 IV 单独返回或顺序不同,需先自行截取再传入) static String decryptFromCipherBytes( String keyBase64, Uint8List cipherBytes) { try { final key = _decodeKey(keyBase64); if (cipherBytes.length < gcmIvLength) { throw ArgumentError( 'Invalid ciphertext: too short (need at least $gcmIvLength bytes for IV)'); } final iv = cipherBytes.sublist(0, gcmIvLength); final cipherText = cipherBytes.sublist(gcmIvLength); final cipher = GCMBlockCipher(AESEngine()); final params = AEADParameters( KeyParameter(key), gcmTagLength * 8, iv, Uint8List(0), ); cipher.init(false, params); final plainText = cipher.process(cipherText); return utf8.decode(plainText); } on InvalidCipherTextException catch (e) { throw Exception( 'Decryption failed: GCM 校验失败(密钥错误、IV/密文格式不符或数据被篡改)。' ' 请确认:① 密钥与加密端一致 ② 密文为「前 12 字节 IV + 密文」。原始: $e', ); } catch (e) { throw Exception('Decryption failed: $e'); } } /// 解密:从 "[169, 246, 173, ...]" 格式的字节列表字符串解密 /// 适用于接口返回的密文是「数字列表字符串」而非 Base64 的情况 /// [keyBase64] Base64 编码的密钥,传 null 则使用 SdkConfig.decryptKey /// [byteListString] 形如 "[169, 246, 173, 197, ...]" 的字符串(前 12 字节为 IV,后续为密文) /// 返回解密后的 UTF-8 字符串 static String decryptFromByteListString(String byteListString, {String? keyBase64}) { final key = keyBase64 ?? _defaultKey; final cipherBytes = _parseByteListString(byteListString); return decryptFromCipherBytes(key, cipherBytes); } /// 将 "[169, 246, ...]" 格式的字符串解析为 Uint8List static Uint8List _parseByteListString(String byteListString) { final list = byteListString .replaceAll(RegExp(r'[\[\]\s]+'), ' ') .split(',') .map((s) => s.trim()) .where((s) => s.isNotEmpty) .map((s) => int.parse(s)) .toList(); for (final b in list) { if (b < 0 || b > 255) throw ArgumentError('Byte value out of range: $b'); } return Uint8List.fromList(list); } /// 解码 Base64 密钥 static Uint8List _decodeKey(String keyBase64) { // 处理 URL 安全格式 String normalizedBase64 = keyBase64.replaceAll('-', '+').replaceAll('_', '/'); // 添加填充 while (normalizedBase64.length % 4 != 0) { normalizedBase64 += '='; } final keyBytes = base64Decode(normalizedBase64); if (keyBytes.length != keySize) { throw ArgumentError('Key must be 32 bytes for AES-256'); } return keyBytes; } /// GZIP 压缩字节数组 static Uint8List gzip(Uint8List data) { return gzipEncode(data); } /// GZIP 压缩字符串并转 Base64 static String gzipToBase64(String str) { if (str.isEmpty) return str; try { final compressed = gzipEncode(utf8.encode(str)); return base64Encode(compressed); } catch (e) { throw Exception('Gzip 压缩失败: $e'); } } /// GZIP 解压字节数组 static Uint8List gunzip(Uint8List data) { return gzipDecode(data); } /// 从 Base64 解压 GZIP static String gunzipFromBase64(String base64) { if (base64.isEmpty) return base64; try { final compressed = base64Decode(base64); final decompressed = gzipDecode(compressed); return utf8.decode(decompressed); } catch (e) { throw Exception('Gzip 解压失败: $e'); } } /// GZIP 编码(使用 package:archive,兼容 Web) static Uint8List gzipEncode(List data) { final encoder = GZipEncoder(); final compressed = encoder.encode(data) ?? []; return Uint8List.fromList(compressed); } /// GZIP 解码(使用 package:archive,兼容 Web) static Uint8List gzipDecode(List data) { final decoder = GZipDecoder(); final decompressed = decoder.decodeBytes(data); return Uint8List.fromList(decompressed); } /// 解密接口响应(使用 SDK 默认密钥) /// /// 接口直接返回加密字符串,整段解密后返回明文。 /// /// [responseBody] 接口返回的加密字符串 /// [useGzip] 是否对解密结果再做 GZIP 解压(默认:false) static String decryptResponseAuto(String responseBody, {bool useGzip = false}) { return decryptResponse(_defaultKey, responseBody, useGzip: useGzip); } /// 默认密钥(从 SdkConfig 获取) static String get _defaultKey => SdkConfig.decryptKey; /// 解密接口响应 /// /// 接口直接返回加密字符串,整段解密后返回明文。 /// /// [keyBase64] Base64 编码的密钥 /// [responseBody] 接口返回的加密字符串 /// [useGzip] 是否对解密结果再做 GZIP 解压(默认:false) static String decryptResponse(String keyBase64, String responseBody, {bool useGzip = false}) { try { final decrypted = decryptToString(keyBase64, responseBody.trim()); if (useGzip) return gunzipFromBase64(decrypted); return decrypted; } catch (e) { if (e is Exception) rethrow; throw Exception('解密响应失败: $e'); } } /// 解密接口响应并解析为 Map /// 接口直接返回加密字符串,解密后再按 JSON 解析为 Map。 /// [keyBase64] Base64 编码的密钥 /// [responseBody] 接口返回的加密字符串 /// [useGzip] 是否对解密结果再做 GZIP 解压(默认:false) static Map decryptResponseToMap( String keyBase64, String responseBody, { bool useGzip = false, }) { final decryptedJson = decryptResponse(keyBase64, responseBody, useGzip: useGzip); return jsonDecode(decryptedJson) as Map; } /// 解密接口响应并解析为指定类型 /// 接口直接返回加密字符串,解密后再按 JSON 解析并转为 T。 /// [keyBase64] Base64 编码的密钥 /// [responseBody] 接口返回的加密字符串 /// [fromJson] JSON 解析函数 /// [useGzip] 是否对解密结果再做 GZIP 解压(默认:false) static T decryptResponseToObject( String keyBase64, String responseBody, T Function(Map) fromJson, { bool useGzip = false, }) { final decryptedMap = decryptResponseToMap(keyBase64, responseBody, useGzip: useGzip); return fromJson(decryptedMap); } }