Initial commit: 91dj android client dev_1.0.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
谢宇宁
2026-10-08 11:37:25 +07:00
co-authored by Claude Opus 5.5
commit 97dd6a739a
1262 changed files with 119813 additions and 0 deletions
+346
View File
@@ -0,0 +1,346 @@
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);
}
}