Files
51dj_android/analytics_sdk/lib/utils/aes_gcm_util.dart
T
2026-10-08 11:37:25 +07:00

347 lines
12 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 '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<int> data) {
final encoder = GZipEncoder();
final compressed = encoder.encode(data) ?? <int>[];
return Uint8List.fromList(compressed);
}
/// GZIP 解码(使用 package:archive,兼容 Web)
static Uint8List gzipDecode(List<int> 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<String, dynamic> decryptResponseToMap(
String keyBase64,
String responseBody, {
bool useGzip = false,
}) {
final decryptedJson =
decryptResponse(keyBase64, responseBody, useGzip: useGzip);
return jsonDecode(decryptedJson) as Map<String, dynamic>;
}
/// 解密接口响应并解析为指定类型
/// 接口直接返回加密字符串,解密后再按 JSON 解析并转为 T。
/// [keyBase64] Base64 编码的密钥
/// [responseBody] 接口返回的加密字符串
/// [fromJson] JSON 解析函数
/// [useGzip] 是否对解密结果再做 GZIP 解压(默认:false)
static T decryptResponseToObject<T>(
String keyBase64,
String responseBody,
T Function(Map<String, dynamic>) fromJson, {
bool useGzip = false,
}) {
final decryptedMap =
decryptResponseToMap(keyBase64, responseBody, useGzip: useGzip);
return fromJson(decryptedMap);
}
}