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