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
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 是否与服务器一致',
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|