abler-api 1.0.78 → 1.0.81

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.
Files changed (2) hide show
  1. package/dist/cjs/pp-util.js +221 -28
  2. package/package.json +7 -7
@@ -291,6 +291,12 @@ class apiUtil$2 {
291
291
  };
292
292
  };
293
293
  static appAsPrefix = '?';
294
+ static cekEncryptOptions = {
295
+ algorithm: "aes-256-gcm",
296
+ // iv: 随机
297
+ encryptEncoding: "base64",
298
+ decryptEncoding: "utf8"
299
+ };
294
300
 
295
301
  // static apiCallRecSaver;
296
302
 
@@ -1362,32 +1368,155 @@ class apiUtil$2 {
1362
1368
  static corsCheckOrigin(origin, callback) {
1363
1369
  callback(null, apiUtil$2.isOriginAllowed(origin));
1364
1370
  }
1371
+ static async restoreObj(key) {
1372
+ return kvStorage.restoreObj(key).catch(() => null);
1373
+ }
1374
+
1375
+ //#region ==== CEK 加解密及签名 ====
1376
+
1377
+ // 应用配置:
1378
+ // appSetting:
1379
+ // e2eEncryptionNeeded
1380
+ // cekRotateTime
1381
+ // errCfg:
1382
+ // ACCESS_REFUSED
1383
+ // CEK_EXPIRED
1384
+ // PUBLIC_ENCRYPT_FAIL
1385
+ // DECRYPT_FAIL
1386
+ // dbSql
1387
+ // APP_QUERY
1388
+
1389
+ static async rotateCek(req) {
1390
+ try {
1391
+ const appInfo = req.appInfo;
1392
+ const apiKey = appInfo.apiKey;
1393
+ let cek = null;
1394
+ if (appSetting.e2eEncryptionNeeded) {
1395
+ await this.storePreviousCek(apiKey, req);
1396
+ // 生成新的CEK
1397
+ cek = await this.generateCek(appInfo, req);
1398
+ console.log("已生成新的CEK", cek);
1399
+ }
1400
+ let result = this.apiSuccess(cek, req);
1401
+ delete result.message; //减少数据尺寸,否则数据太长无法用公钥加密
1402
+ delete result._elapse;
1403
+ // options._res.publicKey = appInfo.publicKey; // 设置用于加密结果的公钥
1404
+ return result;
1405
+ } catch (e) {
1406
+ return apiUtil$2.apiFail(e, req);
1407
+ }
1408
+ }
1409
+ static cekExpired(cek) {
1410
+ return new Date().valueOf() >= cek.expireAt;
1411
+ }
1365
1412
  static validCek(cek, checkExpired = true) {
1366
1413
  if (!cek) {
1367
- throw [errCfg.ACCESS_REFUSED, t("CEK(内容加密密钥)尚未申请或已过期")];
1414
+ throw [this.errCfg.CEK_EXPIRED, t("或尚未申请")];
1368
1415
  }
1369
- if (checkExpired && new Date().valueOf() >= cek.expireAt) {
1370
- throw errCfg.CEK_EXPIRED;
1416
+ if (checkExpired && this.cekExpired(cek)) {
1417
+ throw this.errCfg.CEK_EXPIRED;
1371
1418
  }
1372
1419
  }
1420
+ static encryptContentText(contentText, cek) {
1421
+ return ppCrypto.encryptData(contentText, cek.key, this.cekEncryptOptions);
1422
+ }
1373
1423
  static encryptContent(content, cek) {
1374
- return ppCrypto.encryptData(content, cek.key, 'base64');
1424
+ // let contentText = commonUtil.orderedJsonStringify(content);
1425
+ let contentText = JSON.stringify(content);
1426
+ return this.encryptContentText(contentText, cek);
1427
+ }
1428
+ static encryptResponseContent(response, result) {
1429
+ if (!appSetting.e2eEncryptionNeeded) {
1430
+ if (response.traceId) {
1431
+ console.log(1, response.traceId, 'Response:', result);
1432
+ }
1433
+ return result;
1434
+ }
1435
+ if (response.traceId) {
1436
+ console.log(1, response.traceId, 'Response before encrypt:', result);
1437
+ }
1438
+ const {
1439
+ publicKey,
1440
+ cek
1441
+ } = response;
1442
+ if (cek) {
1443
+ if (response.newCek) {
1444
+ result.newCek = response.newCek;
1445
+ }
1446
+ if (response.traceId) {
1447
+ console.log(1, response.traceId, 'CEK:', cek);
1448
+ }
1449
+ result = {
1450
+ encryptedContent: this.encryptContent(result, cek)
1451
+ };
1452
+ } else if (publicKey) {
1453
+ if (response.traceId) {
1454
+ console.log(1, response.traceId, 'PUBLIC KEY:', publicKey);
1455
+ }
1456
+ const message = Buffer.from(JSON.stringify(result), "utf8");
1457
+ try {
1458
+ result = ppCrypto.rsaEncrypt({
1459
+ publicKey,
1460
+ message
1461
+ });
1462
+ } catch (e) {
1463
+ result = apiUtil$2.apiFail([errCfg.PUBLIC_ENCRYPT_FAIL, e.message], response.req);
1464
+ }
1465
+ }
1466
+ if (response.traceId) {
1467
+ console.log(1, response.traceId, 'Response after encrypt:', result);
1468
+ }
1469
+ return result;
1375
1470
  }
1376
- static decryptContent(content, cek) {
1471
+ static decryptContentText(content, cek) {
1377
1472
  this.validCek(cek);
1378
1473
  try {
1379
- const result = ppCrypto.decryptData(content, cek.key, 'base64', 'utf8');
1380
- if (result.startsWith('{') || result.startsWith('[')) {
1381
- return JSON.parse(result);
1382
- }
1383
- return result;
1474
+ return ppCrypto.decryptData(content, cek.key, this.cekEncryptOptions);
1384
1475
  } catch (e) {
1385
- throw [errCfg.DECRYPT_FAIL, e.message];
1476
+ // console.log("内容", content);
1477
+ // const req = RequestContext.get('req');
1478
+ // console.log("headers", req.headers);
1479
+ // console.error(e);
1480
+ throw [this.errCfg.DECRYPT_FAIL, e.message];
1386
1481
  }
1387
1482
  }
1388
- static decryptRequestContent(req, res) {
1483
+ static decryptContent(content, cek) {
1484
+ let contentText = this.decryptContentText(content, cek);
1485
+ if (contentText.startsWith('{') || contentText.startsWith('[')) {
1486
+ return JSON.parse(contentText);
1487
+ }
1488
+ return contentText;
1489
+ }
1490
+
1491
+ // static decryptRequestContent(req, res) {
1492
+ // if (!appSetting.e2eEncryptionNeeded)
1493
+ // return;
1494
+ // let params = apiUtil.extractParams(req);
1495
+ // if (params.encryptedContent) {
1496
+ // if (['DELETE', 'GET'].contains(req.method)) {
1497
+ // params.encryptedContent = decodeURIComponent(params.encryptedContent);
1498
+ // }
1499
+ // if (!req.appInfo) {
1500
+ // throw t('应用系统信息未知');
1501
+ // }
1502
+ // const decryptedContent = apiUtil.decryptContent(params.encryptedContent, req.appInfo.cek);
1503
+ // delete params.encryptedContent;
1504
+ // Object.assign(req.params, decryptedContent);
1505
+ // }
1506
+ // if (res) {
1507
+ // apiUtil.validCek(req.appInfo.cek);
1508
+ // res.cek = req.appInfo.cek; // 设置用于对称加密的CEK
1509
+ // }
1510
+ // }
1511
+
1512
+ static async decryptRequestContent(req, res) {
1389
1513
  if (!appSetting.e2eEncryptionNeeded) return;
1514
+ const appInfo = req.appInfo;
1390
1515
  let params = apiUtil$2.extractParams(req);
1516
+ if (req.traceId) {
1517
+ console.log(1, req.traceId, 'Request Params before decrypt:', params);
1518
+ console.log(1, req.traceId, 'CEK:', appInfo.cek);
1519
+ }
1391
1520
  if (params.encryptedContent) {
1392
1521
  if (['DELETE', 'GET'].contains(req.method)) {
1393
1522
  params.encryptedContent = decodeURIComponent(params.encryptedContent);
@@ -1395,23 +1524,67 @@ class apiUtil$2 {
1395
1524
  if (!req.appInfo) {
1396
1525
  throw t('应用系统信息未知');
1397
1526
  }
1398
- const decryptedContent = apiUtil$2.decryptContent(params.encryptedContent, req.appInfo.cek);
1527
+ let decryptedContent;
1528
+ try {
1529
+ if (req.traceId) {
1530
+ const dataText = this.decryptContentText(params.encryptedContent, appInfo.cek);
1531
+ console.log(1, req.traceId, 'Decrypted text:', dataText);
1532
+ }
1533
+ decryptedContent = this.decryptContent(params.encryptedContent, appInfo.cek);
1534
+ } catch (e) {
1535
+ let cek = appInfo.cek;
1536
+ console.log("解密失败 CEK", cek?.key, new Date(cek?.expireAt).format('MM.dd hh:mm:ss'));
1537
+ // 尝试用前一个 CEK 解密
1538
+ const previousCek = await this.restorePreviousCek(appInfo.apiKey, req);
1539
+ if (!previousCek) {
1540
+ throw e;
1541
+ }
1542
+ cek = previousCek;
1543
+ console.log("换用前任 CEK", cek.key, new Date(cek.expireAt).format('MM.dd hh:mm:ss'));
1544
+ try {
1545
+ decryptedContent = this.decryptContent(params.encryptedContent, cek);
1546
+ if (res) {
1547
+ res.newCek = appInfo.cek;
1548
+ }
1549
+ appInfo.cek = cek;
1550
+ } catch (e) {
1551
+ console.log("换用前任解密依然失败", "ApiKey", appInfo.apiKey, "x-api-key", req.headers["x-api-key"] || req.headers["api-key"]);
1552
+ throw e;
1553
+ }
1554
+ }
1399
1555
  delete params.encryptedContent;
1400
- Object.assign(req.params, decryptedContent);
1556
+ Object.assign(params, decryptedContent);
1557
+ }
1558
+ if (req.traceId) {
1559
+ console.log(1, req.traceId, 'Request Params after decrypt:', params);
1401
1560
  }
1402
1561
  if (res) {
1403
- apiUtil$2.validCek(req.appInfo.cek);
1404
- res.cek = req.appInfo.cek; // 设置用于对称加密的CEK
1562
+ this.validCek(appInfo.cek);
1563
+ res.cek = appInfo.cek; // 设置用于对称加密的CEK
1405
1564
  }
1406
1565
  }
1407
- static async restoreObj(key) {
1408
- return kvStorage.restoreObj(key).catch(() => null);
1566
+ static async storePreviousCek(apiKey, req) {
1567
+ let cekKey = this.cekStoreKey(apiKey, req);
1568
+ const previousCek = await this.restoreObj(cekKey);
1569
+ if (previousCek && !this.cekExpired(previousCek)) {
1570
+ // 轮换期间保留旧版本一段时间
1571
+ cekKey += "_p";
1572
+ await kvStorage.storeObj(cekKey, previousCek, appSetting.cekRotateTime);
1573
+ }
1574
+ }
1575
+ static async restorePreviousCek(apiKey, req) {
1576
+ let cekKey = this.cekStoreKey(apiKey, req) + "_p";
1577
+ return await this.restoreObj(cekKey);
1409
1578
  }
1410
1579
  static cekStoreKey(apiKey, req) {
1411
- return `cek_${this.appAsPrefix}_${apiKey}_${apiUtil$2.getClientIp(req)}`;
1580
+ // return `cek_${this.appAsPrefix}_${apiKey}_${apiUtil.getClientIp(req)}`
1581
+ let key = `cek_${this.appAsPrefix}_${apiKey}`;
1582
+ if (req?.apiClientId) {
1583
+ return `${key}@${req.apiClientId}`;
1584
+ }
1585
+ return key;
1412
1586
  }
1413
1587
  static async generateCek(appInfo, req, key) {
1414
- //todo: 检查既有 cek
1415
1588
  let t = new Date().valueOf();
1416
1589
  const cek = {
1417
1590
  key: key || ppUtil$3.newGuid() + ppUtil$3.newGuid(),
@@ -1450,7 +1623,7 @@ class apiUtil$2 {
1450
1623
  return apiUtil$2.apiFail(e, req);
1451
1624
  }
1452
1625
  }
1453
- static async restoreAppInfo(apiKey, req) {
1626
+ static async restoreOrLoadAppInfo(apiKey, req) {
1454
1627
  let appInfo = await apiUtil$2.restoreObj(`${this.appAsPrefix}App_${apiKey}`, appSetting.tokenExpireTime);
1455
1628
  if (!appInfo) {
1456
1629
  appInfo = await dbUtil.dbQueryOneAndUnstringify(conf$2.dbSql.APP_QUERY, {
@@ -1458,7 +1631,7 @@ class apiUtil$2 {
1458
1631
  }, "exData");
1459
1632
  if (appInfo) {
1460
1633
  // await generateCek(appInfo);
1461
- await kvStorage.storeObj(`cbpApp_${appInfo.apiKey}`, appInfo, appSetting.tokenExpireTime);
1634
+ await kvStorage.storeObj(`${this.appAsPrefix}App_${apiKey}`, appInfo, appSetting.tokenExpireTime);
1462
1635
  }
1463
1636
  }
1464
1637
  // if (appInfo && !appInfo.cek && appSetting.e2eEncryptionNeeded) {
@@ -1480,7 +1653,7 @@ class apiUtil$2 {
1480
1653
  encryptedContent: ''
1481
1654
  }
1482
1655
  };
1483
- const appInfo = await this.restoreAppInfo(params.apiKey, options._res.req);
1656
+ const appInfo = await this.restoreOrLoadAppInfo(params.apiKey, options._res.req);
1484
1657
  if (!appInfo) {
1485
1658
  throw [errCfg.ACCESS_REFUSED, t_f$2("apiKey (%s) 无效", params.apiKey)];
1486
1659
  }
@@ -1529,16 +1702,34 @@ class apiUtil$2 {
1529
1702
  }
1530
1703
  static async reqAppInfoNeeded(req) {
1531
1704
  if (!req.appInfo) {
1532
- let apiKey = req.headers["api-key"] || req.leInfo?.apiKey || req.userInfo?.apiKey;
1533
- if (!apiKey) {
1534
- throw [errCfg.ACCESS_REFUSED, t("必须在请求头中设置api-key")];
1535
- }
1536
- req.appInfo = await this.restoreAppInfo(apiKey, req);
1705
+ const apiKey = this.extractApiKey(req);
1706
+ req.appInfo = await this.restoreOrLoadAppInfo(apiKey, req);
1537
1707
  if (!req.appInfo) {
1538
1708
  throw [errCfg.ACCESS_REFUSED, t_f$2("api-key (%s) 无效", apiKey)];
1539
1709
  }
1540
1710
  }
1541
1711
  }
1712
+ static extractApiKey(req, noErr) {
1713
+ if (!req.apiKey) {
1714
+ let apiKey = req.headers['x-api-key'] || req.headers['api-key'] || req.leInfo?.apiKey || req.userInfo?.apiKey;
1715
+ if (!apiKey) {
1716
+ if (noErr) return;
1717
+ throw [errCfg.ACCESS_REFUSED, t_f$2("必须在请求头中设置api-key")];
1718
+ }
1719
+
1720
+ // 不能用IP作为客户端主机识别,因为一台主机常常会有多个IP,而每次请求的IP可能会发生变化
1721
+ // 用户在 apiKey 加后缀 @xx 来区分同一ApiKey的不同客户端(服务器主机/进程),或者在请求头中增加 x-api-client-id 来设置客户端标识
1722
+ if (apiKey.includes("@")) {
1723
+ const two = apiKey.split("@");
1724
+ apiKey = two[0];
1725
+ req.apiClientId = two[1];
1726
+ } else {
1727
+ req.apiClientId = req.headers['x-api-client-id'] || req.headers['api-client-id'];
1728
+ }
1729
+ req.apiKey = apiKey;
1730
+ }
1731
+ return req.apiKey;
1732
+ }
1542
1733
  static async verifyApiSignature(req) {
1543
1734
  if (req.appInfo) {
1544
1735
  return true;
@@ -1605,6 +1796,8 @@ class apiUtil$2 {
1605
1796
 
1606
1797
  //#endregion
1607
1798
 
1799
+ //#endregion
1800
+
1608
1801
  //region ====中间件
1609
1802
 
1610
1803
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abler-api",
3
- "version": "1.0.78",
3
+ "version": "1.0.81",
4
4
  "description": "API服务相关工具",
5
5
  "main": "./dist/cjs/pp-util.js",
6
6
  "-module": "./dist/es/pp-util.js",
@@ -15,13 +15,13 @@
15
15
  "author": "peng_peng",
16
16
  "license": "ISC",
17
17
  "dependencies": {
18
- "abler-db": "^1.0.86",
19
- "abler-i18n": "^1.0.21",
20
- "abler-messenger": "^1.1.41",
21
- "abler-net": "^1.0.42",
22
- "abler-util": "^1.0.40",
18
+ "abler-db": "^1.0.88",
19
+ "abler-i18n": "^1.0.22",
20
+ "abler-messenger": "^1.1.43",
21
+ "abler-net": "^1.0.44",
22
+ "abler-util": "^1.0.42",
23
23
  "basic-auth": "^2.0.1",
24
24
  "node-cron": "^3.0.1"
25
25
  },
26
- "gitHead": "5ceb8a2723650bc8c2a3a8c200c1e7441db9dda5"
26
+ "gitHead": "aa1ffa76c3eedf9ec6b343d5b32518a86f7bdfaf"
27
27
  }