@tmsfe/tms-core 0.0.157 → 0.0.159

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.157",
3
+ "version": "0.0.159",
4
4
  "description": "tms运行时框架",
5
5
  "repository": {
6
6
  "type": "git",
@@ -3,6 +3,7 @@
3
3
  * 暴露出来的工具函数封装原则:不可以向外throw错误,返回执行结果统一格式:{ success: boolean, msg: string, res: any }
4
4
  * https://iwiki.woa.com/p/4013041987#1.-RSA+AES-%E5%88%87%E6%8D%A2-Curve25519+XSalsa20%EF%BC%9A
5
5
  */
6
+ import md5 from '../md5';
6
7
  /* eslint-disable @typescript-eslint/no-require-imports */
7
8
  const ecc = require('./nacl.min.js');
8
9
  const base64Util = require('./nacl-util.min.js');
@@ -106,12 +107,10 @@ const eccUtil = {
106
107
  method: 'POST',
107
108
  data,
108
109
  enableHttp2: true,
109
- success: (res) => {
110
- const success = eccUtil._updateGlobalPublicKeyInfo(false, res.header);
111
- resolve(success);
110
+ success: () => {
111
+ resolve(true);
112
112
  },
113
113
  fail: () => {
114
- eccUtil._updateGlobalPublicKeyInfo(true);
115
114
  resolve(false);
116
115
  },
117
116
  });
@@ -135,10 +134,10 @@ const eccUtil = {
135
134
  return new baseUtil.BaseRespFac(success);
136
135
  },
137
136
  _privateKeyInfo: null,
138
- getPrivateKeyInfo: (): CryptoKeyInfo => {
137
+ getPrivateKeyInfo: (forceUpdate = false): CryptoKeyInfo => {
139
138
  if (!wx.$_publicKey) return null;
140
139
  const serverPubInfo = wx.$_publicKey;
141
- if (!eccUtil._privateKeyInfo || eccUtil._privateKeyInfo.serverPubId !== serverPubInfo.pubId) {
140
+ if (forceUpdate || !eccUtil._privateKeyInfo || eccUtil._privateKeyInfo.serverPubId !== serverPubInfo.pubId) {
142
141
  const keyPair = ecc.box.keyPair(); // 生成客户端公钥私钥
143
142
  eccUtil._privateKeyInfo = {
144
143
  serverPubId: serverPubInfo.pubId, // 服务端公钥id
@@ -149,33 +148,91 @@ const eccUtil = {
149
148
  return eccUtil._privateKeyInfo;
150
149
  },
151
150
  // 解析gwCode
151
+ /* eslint-disable complexity */
152
152
  resolveGwCode: async (codeStr: string): Promise<{ retry: boolean, success: boolean }> => {
153
153
  if (!codeStr) return { retry: false, success: true };
154
154
  const code = parseInt(codeStr, 10);
155
155
  switch (code) {
156
156
  case 0:
157
157
  return { retry: false, success: true };
158
- case 11300: // 解密失败
159
- return { retry: false, success: false };
160
- case 11305: // 公钥id无效
158
+ case 11305: // 公钥id无效
161
159
  case 11306: { // 公钥过期
162
- const genPubSuccess = await eccUtil._refreshPubKeyInfo(true); // 1. 获取公钥
163
- if (!genPubSuccess) return { retry: false, success: false };
164
- eccUtil.getPrivateKeyInfo(); // 2. 生成私钥
160
+ // 1. 获取公钥
161
+ eccUtil._refreshPubKeyInfo(true).then((genPubSuccess) => {
162
+ genPubSuccess && eccUtil.getPrivateKeyInfo(); // 2. 生成私钥
163
+ });
165
164
  return { retry: true, success: false };
166
165
  }
167
- case 11307: // 加密未开启
168
- return { retry: false, success: false };
166
+ case 11308: { // 密钥对有问题
167
+ eccUtil.getPrivateKeyInfo(true); // 重新生成私钥
168
+ return { retry: true, success: false };
169
+ }
170
+ case 11300: // 解密失败
169
171
  case 11301:
170
172
  case 11302:
171
173
  case 11303:
172
174
  case 11304:
173
- case 11308:
175
+ case 11307: // 加密未开启
176
+ return { retry: true, success: false };
177
+ case 11309:
174
178
  return { retry: false, success: false };
175
179
  default: // 其他网关错误码
176
180
  return { retry: false, success: true };
177
181
  }
178
182
  },
183
+ _getSignSharedByte: () => baseUtil.decUrl('mEufQpM1n5J8-OZZoJE7ucYMC2suTjfsHUq_6z5cyh8'),
184
+ // 计算客户端加密签名
185
+ getClientCryptoSign: (data = {}, header = {}, sharedByte): string => {
186
+ const obj = baseUtil.formatHeader(Object.assign({}, data, header));
187
+ // 1. 生成签名前的字符串
188
+ const str = Object.keys(obj).filter(item => obj[item])
189
+ .sort()
190
+ .reduce((pre, cur) => {
191
+ pre.push(`${cur}=${obj[cur]}`);
192
+ return pre;
193
+ }, [])
194
+ .join('&');
195
+ // 2. md5
196
+ const md5Str = md5(str, '', 'unit8array');
197
+ const nonce = ecc.randomBytes(ecc.box.nonceLength);
198
+ const encrypted = ecc.box.after(md5Str, nonce, sharedByte);
199
+ const combined = new Uint8Array(nonce.length + encrypted.length);
200
+ combined.set(nonce);
201
+ combined.set(encrypted, nonce.length);
202
+ return baseUtil.encUrl(combined);
203
+ },
204
+ // 验证服务端加密签名
205
+ verifyServerCryptoSign: (traceId: string, resHeader = {}): boolean => {
206
+ const formatHeader = baseUtil.formatHeader(resHeader);
207
+ const signStr = formatHeader['x-crypto-sign'];
208
+ const obj = {
209
+ 'x-encrypt-key': formatHeader['x-encrypt-key'],
210
+ 'x-encrypt-response': formatHeader['x-encrypt-response'],
211
+ 'x-response-header-name': formatHeader['x-response-header-name'],
212
+ 'x-encrypted-headers': formatHeader['x-encrypted-headers'],
213
+ 'x-crypto-enable': formatHeader['x-crypto-enable'],
214
+ 'content-type': formatHeader['content-type'],
215
+ 'x-gateway-code': formatHeader['x-gateway-code'],
216
+ 'x-crypto-pub-id': formatHeader['x-crypto-pub-id'],
217
+ 'x-crypto-pub-key': formatHeader['x-crypto-pub-key'],
218
+ 'x-crypto-pub-exp': formatHeader['x-crypto-pub-exp'],
219
+ 'x-crypto-path': formatHeader['x-crypto-path'],
220
+ 'x-trace-id': traceId,
221
+ };
222
+ const msg = baseUtil.decUrl(signStr);
223
+ const decrypted = ecc.sign.open(msg, eccUtil._getSignSharedByte());
224
+ const str = Object.keys(obj).filter(item => obj[item])
225
+ .sort()
226
+ .reduce((pre, cur) => {
227
+ pre.push(`${cur}=${obj[cur]}`);
228
+ return pre;
229
+ }, [])
230
+ .join('&');
231
+ const preHashArr = md5(str, '', 'unit8array');
232
+ const verified = preHashArr.length === decrypted.length && preHashArr.every((v, i) => v === decrypted[i]);
233
+ return verified;
234
+ },
235
+ /* eslint-enable complexity */
179
236
  execEncrypt: (input: string, ignoreNull = false): BaseResp<{
180
237
  cryptoKeyInfo: CryptoKeyInfo,
181
238
  encryptedContent: any } | null> => {
@@ -355,23 +412,30 @@ const reqEncrypt = (method: string, data: any, header: {
355
412
  if (!success) {
356
413
  return new baseUtil.BaseRespFac(null, false, `请求Header加密失败: ${msg}`);
357
414
  }
415
+ const cryptoHeader = {
416
+ 'X-Crypto-Mode': '2', // ecc加密模式
417
+ 'X-Encrypted-Headers': res.encryptedContent,
418
+ 'X-Encrypt-Pub': res.cryptoKeyInfo.serverPubId,
419
+ 'X-Encrypt-Key': res.cryptoKeyInfo.clientPublicKey,
420
+ 'X-Encrypt-Response': '3', // 加密,二进制
421
+ 'X-Response-Header-Name': encryptedResponseHeaderName,
422
+ 'Content-Type': 'text/plain',
423
+ };
424
+ const cryptoSign = eccUtil.getClientCryptoSign(baseUtil._isObject(finalData) ? finalData : {
425
+ body: finalData,
426
+ }, cryptoHeader, res.cryptoKeyInfo.sharedByte);
358
427
  return new baseUtil.BaseRespFac({
359
428
  cryptoKeyInfo: res.cryptoKeyInfo,
360
429
  data: finalData,
361
430
  header: {
362
- 'Content-Type': 'text/plain',
363
- 'X-Crypto-Mode': '2', // ecc加密模式
364
- 'X-Encrypted-Headers': res.encryptedContent,
365
- 'X-Encrypt-Pub': res.cryptoKeyInfo.serverPubId,
366
- 'X-Encrypt-Key': res.cryptoKeyInfo.clientPublicKey,
367
- 'X-Encrypt-Response': '3', // 加密,二进制
368
- 'X-Response-Header-Name': encryptedResponseHeaderName,
431
+ ...cryptoHeader,
432
+ 'X-Crypto-Sign': cryptoSign,
369
433
  },
370
434
  });
371
435
  };
372
436
  // 解密请求结果
373
- const resDecrypt = async (header, data, cryptoKeyInfo: CryptoKeyInfo): Promise<BaseResp<{
374
- retry: boolean,
437
+ const resDecrypt = async (requestTraceId: string, header, data, cryptoKeyInfo: CryptoKeyInfo): Promise<BaseResp<{
438
+ retry: boolean, // 是否需要明文重试
375
439
  header: any,
376
440
  data: any,
377
441
  }>> => {
@@ -386,17 +450,28 @@ const resDecrypt = async (header, data, cryptoKeyInfo: CryptoKeyInfo): Promise<B
386
450
  'content-type': contentType, // 响应内容类型
387
451
  } = formatHeader;
388
452
  if (!encryptResponseMode || encryptResponseMode === '0') { // 不需要解密,直接返回
389
- return new baseUtil.BaseRespFac({ header, data, retry: false });
453
+ const dataStr = base64Util.encodeUTF8(new Uint8Array(data));
454
+ return new baseUtil.BaseRespFac({
455
+ header,
456
+ data: JSON.parse(dataStr),
457
+ retry: false,
458
+ });
390
459
  }
391
460
  const { retry, success: gwSuccess } = await eccUtil.resolveGwCode(gatewayCode);
392
461
  if (!gwSuccess) {
462
+ // 网关加密流程出现问题,需要验证网关签名
463
+ const verified = eccUtil.verifyServerCryptoSign(requestTraceId, header);
464
+ if (!verified) {
465
+ // 验证失败,表示请求被篡改
466
+ return new baseUtil.BaseRespFac({ header: null, data: null, retry: false }, false, '响应被篡改');
467
+ }
393
468
  return new baseUtil.BaseRespFac({ header: null, data: null, retry }, false, `网关返回错误码: ${gatewayCode}`);
394
469
  }
395
470
  let decryptedHeaders = {};
396
471
  if (encryptedResponseHeaderName) { // 解密响应Header
397
472
  const { success, msg, res } = eccUtil.execDecrypt(baseUtil.decUrl(encryptedHeaders), cryptoKeyInfo);
398
473
  if (!success) {
399
- return new baseUtil.BaseRespFac({ header: null, data: null, retry: false }, false, `解密响应Header失败: ${msg}`);
474
+ return new baseUtil.BaseRespFac({ header: null, data: null, retry: true }, false, `解密响应Header失败: ${msg}`);
400
475
  }
401
476
  decryptedHeaders = JSON.parse(res);
402
477
  }
@@ -404,7 +479,7 @@ const resDecrypt = async (header, data, cryptoKeyInfo: CryptoKeyInfo): Promise<B
404
479
  const cipher = needDecode ? baseUtil.decUrl(data) : new Uint8Array(data);
405
480
  const { success, msg, res } = eccUtil.execDecrypt(cipher, cryptoKeyInfo);
406
481
  if (!success) {
407
- return new baseUtil.BaseRespFac({ header: null, data: null, retry: false }, false, `解密响应Body失败: ${msg}`);
482
+ return new baseUtil.BaseRespFac({ header: null, data: null, retry: true }, false, `解密响应Body失败: ${msg}`);
408
483
  }
409
484
  const decryptedBody = JSON.parse(res); // 解密响应Body
410
485
  return new baseUtil.BaseRespFac({
@@ -417,24 +492,40 @@ const resDecrypt = async (header, data, cryptoKeyInfo: CryptoKeyInfo): Promise<B
417
492
  });
418
493
  } catch (e) {
419
494
  // 因为上面的逻辑有parse, 所以这里再加一个catch
420
- return new baseUtil.BaseRespFac({ header: null, data: null, retry: false }, false, `解密响应Body失败: ${JSON.stringify(e)}`);
495
+ return new baseUtil.BaseRespFac({ header: null, data: null, retry: true }, false, `解密响应Body失败: ${JSON.stringify(e)}`);
421
496
  }
422
497
  };
423
498
  // 处理接下来的请求开关
424
499
  let dealEncryptionSwitching = false;
425
- const dealEncryptionSwitch = async (path: string, resHeader): Promise<void> => {
500
+ const dealEncryptionSwitch = async (path: string, traceId: string, resHeader): Promise<boolean> => {
426
501
  if ((!resHeader || dealEncryptionSwitching)) {
427
502
  return;
428
503
  }
429
504
  dealEncryptionSwitching = true;
430
505
  const formatHeader = baseUtil.formatHeader(resHeader);
431
506
  if (formatHeader['x-crypto-enable'] === '0') {
432
- eccUtil.closeCrypto();
507
+ // 网关加密验签,验签通过才能执行关闭
508
+ const verified = eccUtil.verifyServerCryptoSign(traceId, formatHeader);
509
+ if (verified) {
510
+ eccUtil.closeCrypto();
511
+ }
433
512
  } else if (formatHeader['x-crypto-enable'] === '1') {
434
- `${baseUtil.getSinanHost()}/user/login` === path && eccUtil._updateGlobalPublicKeyInfo(false, formatHeader);
513
+ if ([
514
+ `${baseUtil.getSinanHost()}/user/login`,
515
+ `${baseUtil.getSinanHost()}/basic/crypto/lastkey`,
516
+ ].indexOf(path) > -1) {
517
+ const verified = eccUtil.verifyServerCryptoSign(traceId, formatHeader);
518
+ if (!verified) {
519
+ // 验签失败,表示请求被篡改
520
+ dealEncryptionSwitching = false;
521
+ return false;
522
+ }
523
+ eccUtil._updateGlobalPublicKeyInfo(false, formatHeader);
524
+ }
435
525
  await eccUtil.openCrypto();
436
526
  } // 0是关闭,1是开启, 2是保持
437
527
  dealEncryptionSwitching = false;
528
+ return true;
438
529
  };
439
530
 
440
531
  const encryptUtil = {
@@ -71,6 +71,7 @@ function proxyWxRequest(): void {
71
71
  value(options: any) {
72
72
  const { url, method, data, header = {}, success, fail, complete, dataType, responseType } = options;
73
73
  const traceparent = genTraceparent();
74
+ const traceId = traceparent.split('-')[1];
74
75
  const originalOptions = { ...options };
75
76
 
76
77
  // 如果用户自定义了dataType或者responseType,则不做处理
@@ -79,7 +80,7 @@ function proxyWxRequest(): void {
79
80
  originalRequestApi.call(this, {
80
81
  ...originalOptions,
81
82
  success: (res) => {
82
- encryptUtil.dealEncryptionSwitch(url, res.header);
83
+ encryptUtil.dealEncryptionSwitch(url, traceId, res.header);
83
84
  success?.call(this, res);
84
85
  },
85
86
  header: { ...header, Traceparent: traceparent },
@@ -94,9 +95,14 @@ function proxyWxRequest(): void {
94
95
  util.logInfo(url, traceparent, msg);
95
96
  originalRequestApi.call(this, {
96
97
  ...originalOptions,
97
- success: (res) => {
98
- encryptUtil.dealEncryptionSwitch(url, res.header);
99
- success?.call(this, res);
98
+ success: async (res) => {
99
+ const dealSuccess = await encryptUtil.dealEncryptionSwitch(url, traceId, res.header);
100
+ if (dealSuccess) {
101
+ success?.call(this, res);
102
+ } else {
103
+ util.reportFunc(url, traceparent, `加密验签不通过: ${JSON.stringify(res)}`);
104
+ fail?.call(this, new Error('加密验签不通过'));
105
+ }
100
106
  },
101
107
  header: { ...header, Traceparent: traceparent },
102
108
  });
@@ -115,25 +121,34 @@ function proxyWxRequest(): void {
115
121
  responseType: 'arraybuffer',
116
122
  success: async (result) => {
117
123
  const { header: resHeader, data: resData } = result;
118
- const { success: resSuccess, msg, res } = await encryptUtil.resDecrypt(resHeader, resData, cryptoKeyInfo);
119
- if (resSuccess) {
120
- util.logInfo(url, traceparent, '解密成功');
121
- success?.call(this, res);
122
- const completeRes: any = await completePromp;
123
- completeRes.header = resHeader;
124
- completeRes.data = resData;
125
- complete?.call(this, completeRes);
126
- } else {
124
+ const { success: decSuccess, msg, res } = await encryptUtil
125
+ .resDecrypt(traceId, resHeader, resData, cryptoKeyInfo);
126
+ if (res.retry) { // 解密出现问题,需要明文重试
127
127
  util.reportFunc(url, traceparent, `解密失败:${msg}`);
128
+ const newTraceparent = genTraceparent();
129
+ const traceId = newTraceparent.split('-')[1];
128
130
  originalRequestApi.call(this, {
129
131
  ...originalOptions,
130
132
  success: (res) => {
131
- encryptUtil.dealEncryptionSwitch(url, res.header);
133
+ encryptUtil.dealEncryptionSwitch(url, traceId, res.header);
132
134
  success?.call(this, res);
133
135
  },
134
- header: { ...header, Traceparent: genTraceparent() },
136
+ header: { ...header, Traceparent: newTraceparent },
135
137
  });
138
+ return;
139
+ }
140
+ if (decSuccess) {
141
+ util.logInfo(url, traceparent, '解密成功');
142
+ encryptUtil.dealEncryptionSwitch(url, traceId, resHeader);
143
+ success?.call(this, res);
144
+ } else { // 不支持明文重试,且解密失败
145
+ util.reportFunc(url, traceparent, `解密失败:${msg}`);
146
+ fail?.call(this, new Error(msg));
136
147
  }
148
+ const completeRes: any = await completePromp;
149
+ completeRes.header = resHeader;
150
+ completeRes.data = resData;
151
+ complete?.call(this, completeRes);
137
152
  },
138
153
  fail: async (err) => {
139
154
  fail?.call(this, err);
package/src/md5.js CHANGED
@@ -349,6 +349,20 @@ function hexHMACMD5(k, d) {
349
349
  return rstr2hex(rawHMACMD5(k, d));
350
350
  }
351
351
 
352
+ /**
353
+ * 将原始二进制字符串转换为 Uint8Array
354
+ *
355
+ * @param {string} input 原始的 MD5 二进制字符串
356
+ * @returns {Uint8Array} Uint8Array 格式的 MD5 哈希值
357
+ */
358
+ function rstr2uint8array(input) {
359
+ const output = new Uint8Array(input.length);
360
+ for (let i = 0; i < input.length; i++) {
361
+ output[i] = input.charCodeAt(i);
362
+ }
363
+ return output;
364
+ }
365
+
352
366
  /**
353
367
  * Calculates MD5 value for a given string.
354
368
  * If a key is provided, calculates the HMAC-MD5 value.
@@ -356,7 +370,7 @@ function hexHMACMD5(k, d) {
356
370
  *
357
371
  * @param {string} string Input string
358
372
  * @param {string} [key] HMAC key
359
- * @param {boolean} [raw] Raw output switch
373
+ * @param {string} [raw] Raw output switch
360
374
  * @returns {string} MD5 output
361
375
  */
362
376
  function md5(string, key, raw) {
@@ -364,11 +378,17 @@ function md5(string, key, raw) {
364
378
  if (!raw) {
365
379
  return hexMD5(string);
366
380
  }
381
+ if (raw === 'unit8array') {
382
+ return rstr2uint8array(rawMD5(string));
383
+ }
367
384
  return rawMD5(string);
368
385
  }
369
386
  if (!raw) {
370
387
  return hexHMACMD5(key, string);
371
388
  }
389
+ if (raw === 'unit8array') {
390
+ return rstr2uint8array(rawHMACMD5(key, string));
391
+ }
372
392
  return rawHMACMD5(key, string);
373
393
  }
374
394