@tmsfe/tms-core 0.0.172 → 0.0.174

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmsfe/tms-core",
3
- "version": "0.0.172",
3
+ "version": "0.0.174",
4
4
  "description": "tms运行时框架",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,586 @@
1
+ /**
2
+ * 加密相关的原子工具能力
3
+ * 暴露出来的工具函数封装原则:不可以向外throw错误,返回执行结果统一格式:{ success: boolean, msg: string, res: any }
4
+ * https://iwiki.woa.com/p/4013041987#1.-RSA+AES-%E5%88%87%E6%8D%A2-Curve25519+XSalsa20%EF%BC%9A
5
+ */
6
+ import md5 from './md5';
7
+ /* eslint-disable @typescript-eslint/no-require-imports */
8
+ const ecc = require('./nacl.min.js');
9
+ const base64Util = require('./nacl-util.min.js');
10
+ /* eslint-enable @typescript-eslint/no-require-imports */
11
+
12
+ const logger = wx.getLogManager({});
13
+
14
+ interface BaseResp<T> {
15
+ success: boolean,
16
+ msg: string,
17
+ res: T,
18
+ }
19
+
20
+ interface CryptoKeyInfo {
21
+ sharedByte: Uint8Array,
22
+ clientPublicKey: string,
23
+ serverPubId: string,
24
+ }
25
+
26
+ // 出行接口域名
27
+ const SERVER_HOST_MAP = {
28
+ production: 'https://tim.map.qq.com', // 出行服务正式环境域名
29
+ test: 'https://tim.sparta.html5.qq.com', // 出行服务测试环境域名
30
+ development: 'https://tim.sparta.html5.qq.com/dev', // 出行服务开发环境域名
31
+ predist: 'https://tim.sparta.html5.qq.com/pre', // 出行服务灰度环境域名
32
+ mock: 'http://localhost:8003', // 本地mock环境
33
+ };
34
+
35
+ // 基础工具
36
+ const baseUtil = {
37
+ _isObject: (obj: any): boolean => Object.prototype.toString.call(obj) === '[object Object]',
38
+ // 统一格式化日志输出
39
+ _formatLog(args: any[]): any[] {
40
+ // 小程序日志管理器都只是精确到秒,我们补上毫秒方便分析
41
+ const time = new Date()
42
+ .toISOString()
43
+ .replace('T', ' ')
44
+ .substring(0, 19)
45
+ .replace(/-/g, '-')
46
+ .replace(/:/g, ':');
47
+ args.unshift(time);
48
+ return args;
49
+ },
50
+ logInfo: (...args) => {
51
+ args.unshift('request_encrypt_log');
52
+ const items = baseUtil._formatLog(args);
53
+ // console.log(...items);
54
+ logger.log(...items);
55
+ },
56
+ // Uint8Array转为url安全的base64编码
57
+ encUrl: (input: Uint8Array): string => {
58
+ let base64 = base64Util.encode(input);
59
+ base64 = base64
60
+ .replace(/\+/g, '-')
61
+ .replace(/\//g, '_')
62
+ .replace(/=+$/, '');
63
+ return base64;
64
+ },
65
+ // url安全的base64解码
66
+ decUrl: (input: string): Uint8Array => {
67
+ let base64 = input.replace(/-/g, '+').replace(/_/g, '/');
68
+ while (base64.length % 4) {
69
+ base64 += '=';
70
+ }
71
+ return base64Util.decode(base64);
72
+ },
73
+ // header格式化,将header的key转换为小写
74
+ formatHeader: (header): any => {
75
+ if (!header || !baseUtil._isObject(header)) {
76
+ return {};
77
+ }
78
+ const formatHeader = {};
79
+ Object.keys(header).forEach((key) => {
80
+ formatHeader[key.toLocaleLowerCase()] = header[key];
81
+ });
82
+ return formatHeader;
83
+ },
84
+ formatGetData: (params: any): string => {
85
+ if (!params || !baseUtil._isObject(params)) {
86
+ return '';
87
+ }
88
+ return Object.keys(params)
89
+ .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
90
+ .join('&');
91
+ },
92
+ getSinanHost: (): string => {
93
+ if (wx.$_encryptEnvInfo) {
94
+ return SERVER_HOST_MAP[wx.$_encryptEnvInfo.envName];
95
+ }
96
+ return '';
97
+ },
98
+ getClientEncryptOpen: (): boolean => wx.$_encryptEnvInfo?.requestEncryptOpen,
99
+ BaseRespFac: class BaseRespFac<T> implements BaseResp<T> {
100
+ success: boolean;
101
+ msg: string;
102
+ res: T;
103
+
104
+ constructor(res: T, success?: boolean, msg?: string) {
105
+ this.success = success === undefined ? true : success;
106
+ this.msg = msg === undefined ? '' : msg;
107
+ this.res = res;
108
+ }
109
+ },
110
+ };
111
+ let composeParamsFunc = null; // 请求参数处理函数,加密初始化调用后之后会赋值
112
+ // 加密工具
113
+ const eccUtil = {
114
+ _refreshPromise: null,
115
+ _refreshPubKeyInfo: async (forceRefresh: boolean): Promise<boolean> => {
116
+ // 未完成初始化,则不支持秘钥刷新
117
+ if (!composeParamsFunc) {
118
+ return false;
119
+ }
120
+ if (!wx.$_publicKey || forceRefresh) {
121
+ // eslint-disable-next-line
122
+ eccUtil._refreshPromise = new Promise(async (resolve) => {
123
+ const url = `${baseUtil.getSinanHost()}/basic/crypto/lastkey2`;
124
+ const data = await composeParamsFunc({});
125
+ wx.request({
126
+ url,
127
+ method: 'POST',
128
+ data,
129
+ enableHttp2: true,
130
+ success: () => {
131
+ resolve(true);
132
+ },
133
+ fail: () => {
134
+ resolve(false);
135
+ },
136
+ });
137
+ });
138
+ }
139
+ return eccUtil._refreshPromise;
140
+ },
141
+ // 解析resHeader详情,更新全局公钥信息
142
+ _updateGlobalPublicKeyInfo: (clear: boolean, resHeader?: {[key: string]: any}): BaseResp<boolean> => {
143
+ if (clear) {
144
+ wx.$_publicKey = null;
145
+ return;
146
+ }
147
+ const header = baseUtil.formatHeader(resHeader);
148
+ const {
149
+ 'x-gateway-code': code,
150
+ 'x-crypto-pub-id': pubId, 'x-crypto-pub-key': pubKey, 'x-crypto-pub-exp': pubExp, 'x-crypto-path': pathRule,
151
+ } = header;
152
+ const success = !code && pubId; // login接口会出现接口成功,但是不返回publicKeyInfo的情况
153
+ wx.$_publicKey = success ? { pubId, pubKey, pubExp, pathRule } : null;
154
+ return new baseUtil.BaseRespFac(success);
155
+ },
156
+ _privateKeyInfo: null,
157
+ getPrivateKeyInfo: (forceUpdate = false): CryptoKeyInfo => {
158
+ if (!wx.$_publicKey) return null;
159
+ const serverPubInfo = wx.$_publicKey;
160
+ if (forceUpdate || !eccUtil._privateKeyInfo || eccUtil._privateKeyInfo.serverPubId !== serverPubInfo.pubId) {
161
+ const keyPair = ecc.box.keyPair(); // 生成客户端公钥私钥
162
+ eccUtil._privateKeyInfo = {
163
+ serverPubId: serverPubInfo.pubId, // 服务端公钥id
164
+ sharedByte: ecc.box.before(baseUtil.decUrl(serverPubInfo.pubKey), keyPair.secretKey), // 共享秘钥
165
+ clientPublicKey: baseUtil.encUrl(keyPair.publicKey), // base64 url safe 编码后的客户端公钥
166
+ };
167
+ }
168
+ return eccUtil._privateKeyInfo;
169
+ },
170
+ // 解析gwCode
171
+ /* eslint-disable complexity */
172
+ resolveGwCode: async (codeStr: string): Promise<{ retry: boolean, success: boolean }> => {
173
+ if (!codeStr) return { retry: false, success: true };
174
+ const code = parseInt(codeStr, 10);
175
+ switch (code) {
176
+ case 0:
177
+ return { retry: false, success: true };
178
+ case 11305: // 公钥id无效
179
+ case 11306: { // 公钥过期
180
+ // 1. 获取公钥
181
+ eccUtil._refreshPubKeyInfo(true).then((genPubSuccess) => {
182
+ genPubSuccess && eccUtil.getPrivateKeyInfo(); // 2. 生成私钥
183
+ });
184
+ return { retry: true, success: false };
185
+ }
186
+ case 11308: { // 密钥对有问题
187
+ eccUtil.getPrivateKeyInfo(true); // 重新生成私钥
188
+ return { retry: true, success: false };
189
+ }
190
+ case 11300: // 解密失败
191
+ case 11301:
192
+ case 11302:
193
+ case 11303:
194
+ case 11304:
195
+ case 11307: // 加密未开启
196
+ return { retry: true, success: false };
197
+ case 11309:
198
+ return { retry: false, success: false };
199
+ default: // 其他网关错误码
200
+ return { retry: false, success: true };
201
+ }
202
+ },
203
+ _getSignSharedByte: () => baseUtil.decUrl('mEufQpM1n5J8-OZZoJE7ucYMC2suTjfsHUq_6z5cyh8'),
204
+ // 计算客户端加密签名
205
+ getClientCryptoSign: (data = {}, header = {}, sharedByte): string => {
206
+ const obj = baseUtil.formatHeader(Object.assign({}, data, header));
207
+ // 1. 生成签名前的字符串
208
+ const str = Object.keys(obj).filter(item => obj[item])
209
+ .sort()
210
+ .reduce((pre, cur) => {
211
+ pre.push(`${cur}=${obj[cur]}`);
212
+ return pre;
213
+ }, [])
214
+ .join('&');
215
+ baseUtil.logInfo('---客户端签名---:before', str);
216
+ // 2. md5
217
+ const md5Str = md5(str);
218
+ const nonce = ecc.randomBytes(ecc.box.nonceLength);
219
+ const encrypted = ecc.box.after(md5Str, nonce, sharedByte);
220
+ const combined = new Uint8Array(nonce.length + encrypted.length);
221
+ combined.set(nonce);
222
+ combined.set(encrypted, nonce.length);
223
+ return baseUtil.encUrl(combined);
224
+ },
225
+ // 验证服务端加密签名
226
+ verifyServerCryptoSign: (traceId: string, resHeader = {}): { verified: boolean; msg: string; } => {
227
+ try {
228
+ const formatHeader = baseUtil.formatHeader(resHeader);
229
+ const signStr = formatHeader['x-crypto-sign'];
230
+ if (!signStr) {
231
+ return { verified: false, msg: '服务端无签名信息' };
232
+ }
233
+ const obj = {
234
+ 'x-encrypt-key': formatHeader['x-encrypt-key'],
235
+ 'x-encrypt-response': formatHeader['x-encrypt-response'],
236
+ 'x-response-header-name': formatHeader['x-response-header-name'],
237
+ 'x-encrypted-headers': formatHeader['x-encrypted-headers'],
238
+ 'x-crypto-enable': formatHeader['x-crypto-enable'],
239
+ // 'content-type': formatHeader['content-type'],
240
+ 'x-gateway-code': formatHeader['x-gateway-code'],
241
+ 'x-crypto-pub-id': formatHeader['x-crypto-pub-id'],
242
+ 'x-crypto-pub-key': formatHeader['x-crypto-pub-key'],
243
+ 'x-crypto-pub-exp': formatHeader['x-crypto-pub-exp'],
244
+ 'x-crypto-path': formatHeader['x-crypto-path'],
245
+ 'x-trace-id': traceId,
246
+ };
247
+ const msg = baseUtil.decUrl(signStr);
248
+ const decrypted = ecc.sign.open(msg, eccUtil._getSignSharedByte());
249
+ const str = Object.keys(obj).filter(item => obj[item])
250
+ .sort()
251
+ .reduce((pre, cur) => {
252
+ pre.push(`${cur}=${obj[cur]}`);
253
+ return pre;
254
+ }, [])
255
+ .join('&');
256
+ const preHashArr = md5(str);
257
+ const verified = preHashArr.length === decrypted.length && preHashArr.every((v, i) => v === decrypted[i]);
258
+ return { verified, msg: `客户端验证签名before:${str}` };
259
+ } catch (e) {
260
+ return { verified: false, msg: e.message };
261
+ }
262
+ },
263
+ /* eslint-enable complexity */
264
+ execEncrypt: (input: string, ignoreNull = false): BaseResp<{
265
+ cryptoKeyInfo: CryptoKeyInfo,
266
+ encryptedContent: any } | null> => {
267
+ try {
268
+ const eccKeyInfo = eccUtil.getPrivateKeyInfo();
269
+ if (!eccKeyInfo) {
270
+ return new baseUtil.BaseRespFac(null, false, '加密失败:无加密秘钥信息');
271
+ }
272
+ const cryptoKeyInfo = Object.assign({}, eccKeyInfo); // 因为这里的加密数据要在后续流程中复用,所以需要值拷贝
273
+ if (ignoreNull && (!input || input === '{}')) { // 如果需要忽略空值
274
+ return new baseUtil.BaseRespFac({ cryptoKeyInfo, encryptedContent: '' });
275
+ }
276
+ const { sharedByte } = cryptoKeyInfo;
277
+ const nonce = ecc.randomBytes(ecc.box.nonceLength);
278
+ const encrypted = ecc.box.after(base64Util.decodeUTF8(input), nonce, sharedByte);
279
+ const combined = new Uint8Array(nonce.length + encrypted.length);
280
+ combined.set(nonce);
281
+ combined.set(encrypted, nonce.length);
282
+ const encryptedStr = baseUtil.encUrl(combined);
283
+ return new baseUtil.BaseRespFac({
284
+ cryptoKeyInfo,
285
+ encryptedContent: encryptedStr,
286
+ });
287
+ } catch (e) {
288
+ return new baseUtil.BaseRespFac(null, false, `execEncrypt失败:${JSON.stringify(e)}`);
289
+ }
290
+ },
291
+ execDecrypt: (msg: Uint8Array, cryptoKeyInfo: CryptoKeyInfo): BaseResp<string> => {
292
+ try {
293
+ const { sharedByte } = cryptoKeyInfo;
294
+ const nonce = msg.slice(0, ecc.box.nonceLength);
295
+ const encrypted = msg.slice(ecc.box.nonceLength);
296
+ const decrypted = ecc.box.open.after(encrypted, nonce, sharedByte);
297
+ const decryptedStr = base64Util.encodeUTF8(decrypted);
298
+ return new baseUtil.BaseRespFac(decryptedStr);
299
+ } catch (err) {
300
+ return new baseUtil.BaseRespFac('', false, `execDecrypt失败:${JSON.stringify(err)}`);;
301
+ }
302
+ },
303
+ checkCryptoOpen: (): boolean => !!eccUtil._privateKeyInfo,
304
+ closeCrypto: () => {
305
+ eccUtil._privateKeyInfo = null;
306
+ eccUtil._updateGlobalPublicKeyInfo(true);
307
+ },
308
+ openCrypto: async (): Promise<boolean> => {
309
+ const genPubSuccess = await eccUtil._refreshPubKeyInfo(false); // 1. 获取公钥
310
+ if (!genPubSuccess) return false;
311
+ eccUtil.getPrivateKeyInfo(); // 2. 生成私钥
312
+ return true;
313
+ },
314
+ };
315
+ // 加密规则判断工具
316
+ const cryptRuleUtil = {
317
+ // 远程加密服务是否开启
318
+ isServerOpen: (): boolean => !!wx.$_publicKey,
319
+ // 检查path是否符合下发的路由前缀
320
+ pathInEnablePrefix: (path: string): boolean => {
321
+ if (!wx.$_publicKey) {
322
+ return false;
323
+ }
324
+ const { pathRule } = wx.$_publicKey;
325
+ if (pathRule === '*') {
326
+ return true;
327
+ }
328
+ const prefixArr = pathRule.split(',').map(item => item.trim());
329
+ for (let i = 0, len = prefixArr.length; i < len; i++) {
330
+ if (path.indexOf(prefixArr[i]) > -1) {
331
+ return true;
332
+ }
333
+ }
334
+ return false;
335
+ },
336
+ // 判断是否是性能埋点上报接口
337
+ isPerformanceReport: (path: string, params: any): boolean => {
338
+ // 如果是日志上报接口,需要过滤性能日志,不需要加密
339
+ if (path.indexOf('basic/event/upload') > -1) {
340
+ if (params.batch?.length === 1 && params.batch[0]?.[31] === 'tms-performance-log') {
341
+ return true;
342
+ }
343
+ return false;
344
+ }
345
+ return false;
346
+ },
347
+ // 不参与加密的请求路径规则: { 允许的域名: 不需要加密的path }
348
+ _encryptPathRule: {
349
+ 'tim.map.qq.com': ['^/user/login', '^/api/getClientConfigs', '^/basic/crypto/lastkey2'],
350
+ 'tim.sparta.html5.qq.com': [
351
+ '^/user/login',
352
+ '^/cnabroad', '^~/ReChargeCard/', '^/gasolinerecharge/v2/', '^/gasolinerecharge/rechargecard/',
353
+ '^/tde', '^/basic/crypto/lastkey2',
354
+ ],
355
+ },
356
+ isHostValid: (url) => {
357
+ // 使用正则表达式解析URL
358
+ const urlPattern = /^(https?:\/\/)?([^/?#]+)([/?#].*)?$/;
359
+ const matches = url.match(urlPattern);
360
+ if (!matches) {
361
+ console.error('Invalid URL:', url);
362
+ return false; // 如果URL无效,默认返回false
363
+ }
364
+ const domain = matches[2];
365
+ const path = matches[3] || '/';
366
+ if (cryptRuleUtil._encryptPathRule[domain]) {
367
+ const pathRules = cryptRuleUtil._encryptPathRule[domain];
368
+ for (const rule of pathRules) {
369
+ const regex = new RegExp(rule);
370
+ if (regex.test(path)) {
371
+ return false; // 匹配到规则,不需要加密
372
+ }
373
+ }
374
+ return true; // 此域名下,默认需要加密
375
+ }
376
+ return false; // 没有匹配到规则,不需要加密
377
+ },
378
+ };
379
+
380
+ // 初始化加密工具
381
+ const init = (composeFunc: Function): BaseResp<null> => {
382
+ composeParamsFunc = (...args) => composeFunc(...args); // 加密服务器公共参数
383
+ return new baseUtil.BaseRespFac(null);
384
+ };
385
+
386
+ // 判断是否满足加密规则
387
+ const isCryptoRuleMath = (path: string, reqData: any): BaseResp<boolean> => {
388
+ if (!wx.$_encryptEnvInfo.requestEncryptOpen) {
389
+ return new baseUtil.BaseRespFac(false, false, '本地加密未开启');
390
+ }
391
+ // 如果服务端下发的加密开关关闭,不走加密
392
+ if (!cryptRuleUtil.isServerOpen()) {
393
+ return new baseUtil.BaseRespFac(false, false, '服务端加密未开启');
394
+ }
395
+ // 请求路由不满足服务端下发的加密规则,不走加密
396
+ if (!cryptRuleUtil.pathInEnablePrefix(path)) {
397
+ return new baseUtil.BaseRespFac(false, false, '未命中服务端加密规则');
398
+ }
399
+ // 请求接口是加密性能埋点上报接口,不加密
400
+ if (cryptRuleUtil.isPerformanceReport(path, reqData)) {
401
+ return new baseUtil.BaseRespFac(false, false, '性能埋点');
402
+ }
403
+ // 请求路由不走sinan网关,不加密
404
+ if (!cryptRuleUtil.isHostValid(path)) {
405
+ return new baseUtil.BaseRespFac(false, false, '非sinan网关加密接口');
406
+ }
407
+ return new baseUtil.BaseRespFac(true);;
408
+ };
409
+ // 请求加密
410
+ const reqEncrypt = (method: string, data: any, header: {
411
+ [key: string]: any }, encryptedResponseHeaderName: string): BaseResp<{
412
+ cryptoKeyInfo: CryptoKeyInfo,
413
+ header: any,
414
+ data: any,
415
+ } | null> => {
416
+ const reqHeader = baseUtil.formatHeader(header);
417
+ if (reqHeader.contentType) {
418
+ return new baseUtil.BaseRespFac(null, false, '户自定义了请求contentType');
419
+ }
420
+ let finalData = {};
421
+ // 1. 处理请求参数
422
+ if (method.toUpperCase() === 'GET') {
423
+ const searchParams = baseUtil.formatGetData(data);
424
+ if (!searchParams) {
425
+ return new baseUtil.BaseRespFac(null, false, `GET请求参数不满足加密的规则: ${JSON.stringify(data)}`);
426
+ }
427
+ const { success, msg, res } = eccUtil.execEncrypt(searchParams);
428
+ if (!success) {
429
+ return new baseUtil.BaseRespFac(null, false, `GET请求参数加密失败: ${msg}`);
430
+ }
431
+ finalData = { tmsec: res.encryptedContent };
432
+ } else {
433
+ const { success, msg, res } = eccUtil.execEncrypt(JSON.stringify(data));
434
+ if (!success) {
435
+ return new baseUtil.BaseRespFac(null, false, `${method}请求参数加密失败: ${msg}`);
436
+ }
437
+ finalData = res.encryptedContent;
438
+ }
439
+ // 2. 处理Headers
440
+ const { success, msg, res } = eccUtil.execEncrypt(JSON.stringify(header), true);
441
+ if (!success) {
442
+ return new baseUtil.BaseRespFac(null, false, `请求Header加密失败: ${msg}`);
443
+ }
444
+ const cryptoHeader = {
445
+ 'X-Crypto-Mode': '2', // ecc加密模式
446
+ 'X-Encrypted-Headers': res.encryptedContent,
447
+ 'X-Encrypt-Pub': res.cryptoKeyInfo.serverPubId,
448
+ 'X-Encrypt-Key': res.cryptoKeyInfo.clientPublicKey,
449
+ 'X-Encrypt-Response': '3', // 加密,二进制
450
+ 'X-Response-Header-Name': encryptedResponseHeaderName,
451
+ };
452
+ const cryptoSign = eccUtil.getClientCryptoSign(baseUtil._isObject(finalData) ? finalData : {
453
+ body: finalData,
454
+ }, cryptoHeader, res.cryptoKeyInfo.sharedByte);
455
+ return new baseUtil.BaseRespFac({
456
+ cryptoKeyInfo: res.cryptoKeyInfo,
457
+ data: finalData,
458
+ header: {
459
+ ...cryptoHeader,
460
+ 'Content-Type': 'text/plain',
461
+ 'X-Crypto-Sign': cryptoSign,
462
+ },
463
+ });
464
+ };
465
+ // 解密请求结果
466
+ const resDecrypt = async (requestTraceId: string, header, data, cryptoKeyInfo: CryptoKeyInfo): Promise<BaseResp<{
467
+ retry: boolean, // 是否需要明文重试
468
+ header: any,
469
+ data: any,
470
+ }>> => {
471
+ try {
472
+ const formatHeader = baseUtil.formatHeader(header);
473
+ const {
474
+ // 'x-encrypt-key': clientPublicKey, // base64 url safe编码后的客户端公钥
475
+ 'x-encrypt-response': encryptResponseMode, // 期望的响应加密模式 2: 加密、不压缩 3: 加密、二进制
476
+ 'x-response-header-name': encryptedResponseHeaderName, // 需要加密的响应Header: X-Header1,X-Header2
477
+ 'x-encrypted-headers': encryptedHeaders, // 加密后的Header信息
478
+ 'x-gateway-code': gatewayCode, // 网关返回的错误码
479
+ 'content-type': contentType, // 响应内容类型
480
+ } = formatHeader;
481
+ if (!encryptResponseMode || encryptResponseMode === '0') { // 不需要解密,直接返回
482
+ const dataStr = base64Util.encodeUTF8(new Uint8Array(data));
483
+ return new baseUtil.BaseRespFac({
484
+ header,
485
+ data: JSON.parse(dataStr),
486
+ retry: false,
487
+ });
488
+ }
489
+ const { retry, success: gwSuccess } = await eccUtil.resolveGwCode(gatewayCode);
490
+ if (!gwSuccess) {
491
+ // 网关加密流程出现问题,需要验证网关签名
492
+ const { verified, msg } = eccUtil.verifyServerCryptoSign(requestTraceId, header);
493
+ if (!verified) {
494
+ // 验证失败,表示请求被篡改
495
+ return new baseUtil.BaseRespFac({ header: null, data: null, retry: false }, false, `响应被篡改:${msg}`);
496
+ }
497
+ return new baseUtil.BaseRespFac({ header: null, data: null, retry }, false, `网关返回错误码: ${gatewayCode}`);
498
+ }
499
+ let decryptedHeaders = {};
500
+ if (encryptedResponseHeaderName) { // 解密响应Header
501
+ const { success, msg, res } = eccUtil.execDecrypt(baseUtil.decUrl(encryptedHeaders), cryptoKeyInfo);
502
+ if (!success) {
503
+ return new baseUtil.BaseRespFac({ header: null, data: null, retry: true }, false, `解密响应Header失败: ${msg}`);
504
+ }
505
+ decryptedHeaders = JSON.parse(res);
506
+ }
507
+ const needDecode = contentType.indexOf('text/plain') > -1;
508
+ const cipher = needDecode ? baseUtil.decUrl(data) : new Uint8Array(data);
509
+ const { success, msg, res } = eccUtil.execDecrypt(cipher, cryptoKeyInfo);
510
+ if (!success) {
511
+ return new baseUtil.BaseRespFac({ header: null, data: null, retry: true }, false, `解密响应Body失败: ${msg}`);
512
+ }
513
+ const decryptedBody = JSON.parse(res); // 解密响应Body
514
+ return new baseUtil.BaseRespFac({
515
+ retry: false,
516
+ header: {
517
+ ...header,
518
+ ...decryptedHeaders,
519
+ },
520
+ data: decryptedBody,
521
+ });
522
+ } catch (e) {
523
+ // 因为上面的逻辑有parse, 所以这里再加一个catch
524
+ return new baseUtil.BaseRespFac({ header: null, data: null, retry: true }, false, `解密响应Body失败: ${JSON.stringify(e)}`);
525
+ }
526
+ };
527
+ // 处理接下来的请求开关
528
+ let dealEncryptionSwitching = false;
529
+ const dealEncryptionSwitch = async (path: string, traceId: string, resHeader): Promise<string> => {
530
+ if ((!resHeader || dealEncryptionSwitching)) {
531
+ return '';
532
+ }
533
+ dealEncryptionSwitching = true;
534
+ const formatHeader = baseUtil.formatHeader(resHeader);
535
+ // 加密关闭或者`login接口和lastkey接口`,都需要先执行验签
536
+ const cryptoDisabled = formatHeader['x-crypto-enable'] === '0';
537
+ if ((eccUtil.checkCryptoOpen() && cryptoDisabled)) {
538
+ const { verified, msg } = eccUtil.verifyServerCryptoSign(traceId, formatHeader);
539
+ if (!verified) {
540
+ // 验签失败,表示响应被篡改
541
+ dealEncryptionSwitching = false;
542
+ return `验签失败: ${msg}`;
543
+ }
544
+ }
545
+ if (cryptoDisabled) {
546
+ eccUtil.closeCrypto();
547
+ } else if (formatHeader['x-crypto-enable'] === '1') {
548
+ await eccUtil.openCrypto();
549
+ } // 0是关闭,1是开启, 2是保持
550
+ dealEncryptionSwitching = false;
551
+ return '';
552
+ };
553
+
554
+ /**
555
+ * 处理非加密请求的响应
556
+ * @params path traceId resHeader reqData
557
+ * @returns 是否需要根据响应内容处理加密开关
558
+ */
559
+ const dealRes = (path: string, traceId: string, resHeader, reqData): BaseResp<boolean> => {
560
+ const specialPath = [
561
+ `${baseUtil.getSinanHost()}/user/login`,
562
+ `${baseUtil.getSinanHost()}/basic/crypto/lastkey2`,
563
+ ].indexOf(path) > -1;
564
+ if (specialPath) {
565
+ const formatHeader = baseUtil.formatHeader(resHeader);
566
+ const { verified, msg } = eccUtil.verifyServerCryptoSign(traceId, formatHeader);
567
+ if (!verified) {
568
+ // 验签失败,表示响应被篡改
569
+ return new baseUtil.BaseRespFac(false, false, `验签失败: ${msg}`);
570
+ }
571
+ eccUtil._updateGlobalPublicKeyInfo(false, resHeader);
572
+ }
573
+ return new baseUtil.BaseRespFac(!cryptRuleUtil.isPerformanceReport(path, reqData));
574
+ };
575
+
576
+ const encryptUtil = {
577
+ init, // 初始化加密工具
578
+ isCryptoRuleMath, // 请求是否符合加密规则
579
+ logInfo: baseUtil.logInfo, // 本地日志打印
580
+ dealRes, // 处理不加密请求的响应
581
+ reqEncrypt, // 请求加密:header和data
582
+ resDecrypt, // 响应解密
583
+ dealEncryptionSwitch, // 处理加密开关
584
+ };
585
+
586
+ export default encryptUtil;