You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

86 lines
3.7 KiB

import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:encrypt/encrypt.dart' as enc;
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../../../core/utils/logger.dart';
/// 网关「加密接口」的响应解密。
///
/// 服务端(modules/gateway/wservice_comp.go `writeReply`)对声明在 `EncryptMsgs` 里的接口
/// (`user_getappconfig_v2/v3`、`user_getthirdsvcs_v2`、`user_getagents_v2/v3`)把**整个响应体**
/// 做 AES-CBC 加密:key = 网关的 `GATEWAY_ENCRYPT_KEY`(16/24/32 字节),IV 固定 16 个 `'0'`,
/// PKCS5/7 填充,密文 base64;`Content-Type` 改成 `text/plain`,并加响应头 `X-Encrypted: 1`。
/// 这些响应带着第三方服务的**明文凭据**,所以整体加密后才出网关。
///
/// 密钥来自客户端 `.env` 的 `GATEWAY_ENCRYPT_KEY`,须与服务器一致。没配时 [enabled] 为 false,
/// `Api.getappconfig` 会回退到明文 v1——凭据只能走 env 兼容层,`thirdsvcs` 拿不到。
class ApiCrypto {
ApiCrypto._();
static const String _iv = '0000000000000000';
static String get _key => (dotenv.env['GATEWAY_ENCRYPT_KEY'] ?? '').trim();
/// 密钥是否配置且长度合法(AES 只接受 16/24/32 字节)。
static bool get enabled {
final k = _key;
return k.length == 16 || k.length == 24 || k.length == 32;
}
/// 解 base64(AES-CBC-PKCS7) 密文为明文字符串。密钥不合法或密文损坏时抛异常,由调用方决定退路。
static String decrypt(String cipherBase64) {
final key = enc.Key.fromUtf8(_key);
final iv = enc.IV.fromUtf8(_iv);
final aes = enc.Encrypter(enc.AES(key, mode: enc.AESMode.cbc, padding: 'PKCS7'));
return aes.decrypt64(cipherBase64.trim(), iv: iv);
}
}
/// Dio 拦截器:把 `X-Encrypted: 1` 的响应体解密并 JSON 解析,再交给后面的 [AuthInterceptor]
/// 按业务 JSON 处理。**必须排在 AuthInterceptor 之前**——它一看到不是 JSON 的 body 就当解析失败了。
///
/// 密文的 Content-Type 是 text/plain,Dio 的 `ResponseType.json` 不会去解析它,这里拿到的就是 String。
class ApiDecryptInterceptor extends Interceptor {
static const String _tag = 'ApiCrypto';
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
final flag = response.headers.value('x-encrypted');
if (flag != '1') {
handler.next(response);
return;
}
final body = response.data;
if (body is! String || body.isEmpty) {
handler.next(response);
return;
}
if (!ApiCrypto.enabled) {
// 服务端加密了、客户端却没配密钥:这不是网络问题,得让人一眼看到。
Logger.e(_tag, '响应已加密但 .env 未配置 GATEWAY_ENCRYPT_KEY:${response.requestOptions.path}');
handler.reject(DioException(
requestOptions: response.requestOptions,
response: response,
type: DioExceptionType.badResponse,
error: '响应已加密但客户端未配置 GATEWAY_ENCRYPT_KEY',
));
return;
}
try {
final plain = ApiCrypto.decrypt(body);
response.data = jsonDecode(plain);
handler.next(response);
} catch (e) {
// 密钥不一致时 AES 解出来是乱码、PKCS7 去填充就会失败——报错信息要指向密钥,别让人去查网络。
Logger.e(_tag, '解密失败(多半是 GATEWAY_ENCRYPT_KEY 与服务器不一致):${response.requestOptions.path} $e');
handler.reject(DioException(
requestOptions: response.requestOptions,
response: response,
type: DioExceptionType.badResponse,
error: '响应解密失败,请检查 GATEWAY_ENCRYPT_KEY 是否与服务器一致',
));
}
}
}