@h-ai/crypto 0.1.0-alpha.67 → 0.1.0-alpha.68
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/README.en.md +63 -16
- package/README.ja.md +63 -16
- package/README.md +63 -16
- package/dist/index.d.ts +1625 -173
- package/dist/index.js +719 -230
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -35,7 +35,15 @@ var en_US_default = {
|
|
|
35
35
|
crypto_hashFormatInvalid: "Invalid hash format",
|
|
36
36
|
crypto_passwordVerifyFailed: "Password verification failed",
|
|
37
37
|
crypto_transportClientKeyGenerateFailed: "Failed to generate transport client key pair",
|
|
38
|
-
|
|
38
|
+
crypto_transportServerKeyRingInvalid: "Transport server key ring is missing or invalid",
|
|
39
|
+
crypto_transportServerKeyUnknown: "Transport server key version is unknown or revoked",
|
|
40
|
+
crypto_transportKeyStoreRequired: "Transport encryption requires an explicit client public key store",
|
|
41
|
+
crypto_transportKeyStoreTtlInvalid: "Public-key store TTL must be an integer between 1 and 2147483647 seconds",
|
|
42
|
+
crypto_transportKeyStoreCapacityInvalid: "Public-key store capacity must be a positive safe integer",
|
|
43
|
+
crypto_transportKeyStoreCapacityMismatch: "Public-key store shared capacity settings do not match",
|
|
44
|
+
crypto_transportKeyStoreCapacityReached: "Public-key store capacity has been reached",
|
|
45
|
+
crypto_transportKeyStoreOperationFailed: "Public-key store operation failed",
|
|
46
|
+
crypto_transportAuthenticationFailed: "Transport authentication, request binding or replay validation failed",
|
|
39
47
|
crypto_transportKeyExchangeRequestFailed: "Transport key exchange request failed",
|
|
40
48
|
crypto_transportKeyExchangeHttpFailed: "Transport key exchange returned HTTP {status}",
|
|
41
49
|
crypto_transportKeyExchangeInvalidJson: "Transport key exchange response is not valid JSON",
|
|
@@ -50,6 +58,61 @@ var en_US_default = {
|
|
|
50
58
|
crypto_transportSessionKeyDecryptFailed: "Failed to decrypt transport session key"
|
|
51
59
|
};
|
|
52
60
|
|
|
61
|
+
// messages/ja-JP.json
|
|
62
|
+
var ja_JP_default = {
|
|
63
|
+
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
64
|
+
crypto_notInitialized: "\u6697\u53F7\u30E2\u30B8\u30E5\u30FC\u30EB\u304C\u521D\u671F\u5316\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u5148\u306Bcrypto.init()\u3092\u547C\u3073\u51FA\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
65
|
+
crypto_initFailed: "\u6697\u53F7\u30E2\u30B8\u30E5\u30FC\u30EB\u306E\u521D\u671F\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
66
|
+
crypto_sm2PublicKeyInvalid: "SM2\u516C\u958B\u9375\u306E\u5F62\u5F0F\u304C\u7121\u52B9\u3067\u3059",
|
|
67
|
+
crypto_sm2PrivateKeyInvalid: "SM2\u79D8\u5BC6\u9375\u306E\u5F62\u5F0F\u304C\u7121\u52B9\u3067\u3059",
|
|
68
|
+
crypto_sm2EncryptEmpty: "SM2\u6697\u53F7\u5316\u306E\u7D50\u679C\u304C\u7A7A\u3067\u3059",
|
|
69
|
+
crypto_sm2DecryptFailed: "SM2\u5FA9\u53F7\u306B\u5931\u6557\u3057\u305F\u304B\u3001\u7D50\u679C\u304C\u7121\u52B9\u3067\u3059",
|
|
70
|
+
crypto_sm2SignEmpty: "SM2\u7F72\u540D\u306E\u7D50\u679C\u304C\u7A7A\u3067\u3059",
|
|
71
|
+
crypto_sm2KeyPairGenerateFailed: "SM2\u9375\u30DA\u30A2\u306E\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
72
|
+
crypto_sm2EncryptFailed: "SM2\u6697\u53F7\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
73
|
+
crypto_sm2DecryptFailedWithError: "SM2\u5FA9\u53F7\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
74
|
+
crypto_sm2SignFailed: "SM2\u7F72\u540D\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
75
|
+
crypto_sm2VerifyFailed: "SM2\u7F72\u540D\u306E\u691C\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
76
|
+
crypto_sm3HashEmpty: "SM3\u30CF\u30C3\u30B7\u30E5\u306E\u7D50\u679C\u304C\u7A7A\u3067\u3059",
|
|
77
|
+
crypto_sm3HashFailed: "SM3\u30CF\u30C3\u30B7\u30E5\u306E\u8A08\u7B97\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
78
|
+
crypto_sm3HmacFailed: "HMAC-SM3\u306E\u8A08\u7B97\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
79
|
+
crypto_sm3VerifyFailed: "SM3\u30CF\u30C3\u30B7\u30E5\u306E\u691C\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
80
|
+
crypto_sm4KeyInvalid: "SM4\u9375\u306F16\u30D0\u30A4\u30C8\uFF0816\u9032\u657032\u6587\u5B57\uFF09\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059",
|
|
81
|
+
crypto_sm4CbcNeedIv: "CBC\u30E2\u30FC\u30C9\u306B\u306FIV\u304C\u5FC5\u8981\u3067\u3059",
|
|
82
|
+
crypto_sm4IvInvalid: "SM4\u306EIV\u306F16\u30D0\u30A4\u30C8\uFF0816\u9032\u657032\u6587\u5B57\uFF09\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059",
|
|
83
|
+
crypto_sm4EncryptEmpty: "SM4\u6697\u53F7\u5316\u306E\u7D50\u679C\u304C\u7A7A\u3067\u3059",
|
|
84
|
+
crypto_sm4DecryptFailed: "SM4\u5FA9\u53F7\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
85
|
+
crypto_sm4EncryptFailed: "SM4\u6697\u53F7\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
86
|
+
crypto_sm4DecryptFailedWithError: "SM4\u5FA9\u53F7\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
87
|
+
crypto_passwordEmpty: "\u30D1\u30B9\u30EF\u30FC\u30C9\u3092\u7A7A\u306B\u3059\u308B\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093",
|
|
88
|
+
crypto_passwordHashEmpty: "\u30D1\u30B9\u30EF\u30FC\u30C9\u3068\u30CF\u30C3\u30B7\u30E5\u3092\u7A7A\u306B\u3059\u308B\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093",
|
|
89
|
+
crypto_passwordHashFailed: "\u30D1\u30B9\u30EF\u30FC\u30C9\u306E\u30CF\u30C3\u30B7\u30E5\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
90
|
+
crypto_hashFormatInvalid: "\u30CF\u30C3\u30B7\u30E5\u306E\u5F62\u5F0F\u304C\u7121\u52B9\u3067\u3059",
|
|
91
|
+
crypto_passwordVerifyFailed: "\u30D1\u30B9\u30EF\u30FC\u30C9\u306E\u691C\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
92
|
+
crypto_transportClientKeyGenerateFailed: "\u901A\u4FE1\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u306E\u9375\u30DA\u30A2\u306E\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
93
|
+
crypto_transportServerKeyRingInvalid: "\u30C8\u30E9\u30F3\u30B9\u30DD\u30FC\u30C8\u306E\u30B5\u30FC\u30D0\u30FC\u30AD\u30FC\u30EA\u30F3\u30B0\u304C\u672A\u8A2D\u5B9A\u307E\u305F\u306F\u7121\u52B9\u3067\u3059",
|
|
94
|
+
crypto_transportServerKeyUnknown: "\u30C8\u30E9\u30F3\u30B9\u30DD\u30FC\u30C8\u306E\u30B5\u30FC\u30D0\u30FC\u30AD\u30FC\u30D0\u30FC\u30B8\u30E7\u30F3\u304C\u4E0D\u660E\u3001\u307E\u305F\u306F\u5931\u52B9\u3057\u3066\u3044\u307E\u3059",
|
|
95
|
+
crypto_transportKeyStoreRequired: "\u30C8\u30E9\u30F3\u30B9\u30DD\u30FC\u30C8\u6697\u53F7\u5316\u306B\u306F\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u516C\u958B\u30AD\u30FC\u30B9\u30C8\u30A2\u306E\u660E\u793A\u7684\u306A\u6307\u5B9A\u304C\u5FC5\u8981\u3067\u3059",
|
|
96
|
+
crypto_transportKeyStoreTtlInvalid: "\u516C\u958B\u30AD\u30FC\u30B9\u30C8\u30A2\u306E TTL \u306F1\u301C2147483647\u79D2\u306E\u6574\u6570\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
97
|
+
crypto_transportKeyStoreCapacityInvalid: "\u516C\u958B\u30AD\u30FC\u30B9\u30C8\u30A2\u306E\u5BB9\u91CF\u306F\u6B63\u306E\u5B89\u5168\u306A\u6574\u6570\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
98
|
+
crypto_transportKeyStoreCapacityMismatch: "\u516C\u958B\u30AD\u30FC\u30B9\u30C8\u30A2\u306E\u5171\u6709\u5BB9\u91CF\u8A2D\u5B9A\u304C\u4E00\u81F4\u3057\u307E\u305B\u3093",
|
|
99
|
+
crypto_transportKeyStoreCapacityReached: "\u516C\u958B\u30AD\u30FC\u30B9\u30C8\u30A2\u306E\u5BB9\u91CF\u4E0A\u9650\u306B\u9054\u3057\u307E\u3057\u305F",
|
|
100
|
+
crypto_transportKeyStoreOperationFailed: "\u516C\u958B\u30AD\u30FC\u30B9\u30C8\u30A2\u306E\u64CD\u4F5C\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
101
|
+
crypto_transportAuthenticationFailed: "\u30C8\u30E9\u30F3\u30B9\u30DD\u30FC\u30C8\u8A8D\u8A3C\u3001\u30EA\u30AF\u30A8\u30B9\u30C8\u3068\u306E\u7D10\u4ED8\u3051\u3001\u307E\u305F\u306F\u30EA\u30D7\u30EC\u30A4\u691C\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
102
|
+
crypto_transportKeyExchangeRequestFailed: "\u901A\u4FE1\u306E\u9375\u4EA4\u63DB\u30EA\u30AF\u30A8\u30B9\u30C8\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
103
|
+
crypto_transportKeyExchangeHttpFailed: "\u901A\u4FE1\u306E\u9375\u4EA4\u63DB\u3067HTTP {status}\u304C\u8FD4\u3055\u308C\u307E\u3057\u305F",
|
|
104
|
+
crypto_transportKeyExchangeInvalidJson: "\u901A\u4FE1\u306E\u9375\u4EA4\u63DB\u30EC\u30B9\u30DD\u30F3\u30B9\u304C\u6709\u52B9\u306AJSON\u3067\u306F\u3042\u308A\u307E\u305B\u3093",
|
|
105
|
+
crypto_transportKeyExchangeMissingFields: "\u901A\u4FE1\u306E\u9375\u4EA4\u63DB\u30EC\u30B9\u30DD\u30F3\u30B9\u306B\u5FC5\u9808\u30D5\u30A3\u30FC\u30EB\u30C9\u304C\u3042\u308A\u307E\u305B\u3093",
|
|
106
|
+
crypto_transportClientNotInitialized: "\u901A\u4FE1\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u306E\u9375\u4EA4\u63DB\u304C\u5B8C\u4E86\u3057\u3066\u3044\u307E\u305B\u3093",
|
|
107
|
+
crypto_transportClientNotRegistered: "\u901A\u4FE1\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u304C\u767B\u9332\u3055\u308C\u3066\u3044\u306A\u3044\u304B\u3001\u6709\u52B9\u671F\u9650\u304C\u5207\u308C\u3066\u3044\u307E\u3059",
|
|
108
|
+
crypto_transportRequestEncryptFailed: "\u901A\u4FE1\u30EA\u30AF\u30A8\u30B9\u30C8\u672C\u6587\u306E\u6697\u53F7\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
109
|
+
crypto_transportRequestDecryptFailed: "\u901A\u4FE1\u30EA\u30AF\u30A8\u30B9\u30C8\u672C\u6587\u306E\u5FA9\u53F7\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
110
|
+
crypto_transportResponseEncryptFailed: "\u901A\u4FE1\u30EC\u30B9\u30DD\u30F3\u30B9\u672C\u6587\u306E\u6697\u53F7\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
111
|
+
crypto_transportResponseDecryptFailed: "\u901A\u4FE1\u30EC\u30B9\u30DD\u30F3\u30B9\u672C\u6587\u306E\u5FA9\u53F7\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
112
|
+
crypto_transportSessionKeyEncryptFailed: "\u901A\u4FE1\u30BB\u30C3\u30B7\u30E7\u30F3\u9375\u306E\u6697\u53F7\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
113
|
+
crypto_transportSessionKeyDecryptFailed: "\u901A\u4FE1\u30BB\u30C3\u30B7\u30E7\u30F3\u9375\u306E\u5FA9\u53F7\u306B\u5931\u6557\u3057\u307E\u3057\u305F"
|
|
114
|
+
};
|
|
115
|
+
|
|
53
116
|
// messages/zh-CN.json
|
|
54
117
|
var zh_CN_default = {
|
|
55
118
|
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
@@ -82,7 +145,15 @@ var zh_CN_default = {
|
|
|
82
145
|
crypto_hashFormatInvalid: "\u65E0\u6548\u7684\u54C8\u5E0C\u683C\u5F0F",
|
|
83
146
|
crypto_passwordVerifyFailed: "\u5BC6\u7801\u9A8C\u8BC1\u5931\u8D25",
|
|
84
147
|
crypto_transportClientKeyGenerateFailed: "\u4F20\u8F93\u52A0\u5BC6\u5BA2\u6237\u7AEF\u5BC6\u94A5\u751F\u6210\u5931\u8D25",
|
|
85
|
-
|
|
148
|
+
crypto_transportServerKeyRingInvalid: "\u670D\u52A1\u7AEF\u4F20\u8F93\u5BC6\u94A5\u73AF\u7F3A\u5931\u6216\u65E0\u6548",
|
|
149
|
+
crypto_transportServerKeyUnknown: "\u670D\u52A1\u7AEF\u4F20\u8F93\u5BC6\u94A5\u7248\u672C\u672A\u77E5\u6216\u5DF2\u64A4\u9500",
|
|
150
|
+
crypto_transportKeyStoreRequired: "\u4F20\u8F93\u52A0\u5BC6\u5FC5\u987B\u663E\u5F0F\u63D0\u4F9B\u5BA2\u6237\u7AEF\u516C\u94A5\u5B58\u50A8",
|
|
151
|
+
crypto_transportKeyStoreTtlInvalid: "\u516C\u94A5\u5B58\u50A8 TTL \u5FC5\u987B\u4E3A 1 \u5230 2147483647 \u4E4B\u95F4\u7684\u6574\u6570\u79D2",
|
|
152
|
+
crypto_transportKeyStoreCapacityInvalid: "\u516C\u94A5\u5B58\u50A8\u5BB9\u91CF\u5FC5\u987B\u4E3A\u6B63\u5B89\u5168\u6574\u6570",
|
|
153
|
+
crypto_transportKeyStoreCapacityMismatch: "\u516C\u94A5\u5B58\u50A8\u7684\u5171\u4EAB\u5BB9\u91CF\u914D\u7F6E\u4E0D\u4E00\u81F4",
|
|
154
|
+
crypto_transportKeyStoreCapacityReached: "\u516C\u94A5\u5B58\u50A8\u5BB9\u91CF\u5DF2\u6EE1",
|
|
155
|
+
crypto_transportKeyStoreOperationFailed: "\u516C\u94A5\u5B58\u50A8\u64CD\u4F5C\u5931\u8D25",
|
|
156
|
+
crypto_transportAuthenticationFailed: "\u4F20\u8F93\u6D88\u606F\u8BA4\u8BC1\u3001\u8BF7\u6C42\u7ED1\u5B9A\u6216\u9632\u91CD\u653E\u6821\u9A8C\u5931\u8D25",
|
|
86
157
|
crypto_transportKeyExchangeRequestFailed: "\u4F20\u8F93\u52A0\u5BC6\u5BC6\u94A5\u534F\u5546\u8BF7\u6C42\u5931\u8D25",
|
|
87
158
|
crypto_transportKeyExchangeHttpFailed: "\u4F20\u8F93\u52A0\u5BC6\u5BC6\u94A5\u534F\u5546\u8FD4\u56DE HTTP {status}",
|
|
88
159
|
crypto_transportKeyExchangeInvalidJson: "\u4F20\u8F93\u52A0\u5BC6\u5BC6\u94A5\u534F\u5546\u54CD\u5E94\u4E0D\u662F\u6709\u6548 JSON",
|
|
@@ -100,7 +171,8 @@ var zh_CN_default = {
|
|
|
100
171
|
// src/crypto-i18n.ts
|
|
101
172
|
var cryptoM = core.i18n.createMessageGetter({
|
|
102
173
|
"zh-CN": zh_CN_default,
|
|
103
|
-
"en-US": en_US_default
|
|
174
|
+
"en-US": en_US_default,
|
|
175
|
+
"ja-JP": ja_JP_default
|
|
104
176
|
});
|
|
105
177
|
var CryptoErrorInfo = {
|
|
106
178
|
INVALID_INPUT: "002:400",
|
|
@@ -241,6 +313,13 @@ function createPasswordFunctions(deps) {
|
|
|
241
313
|
}
|
|
242
314
|
};
|
|
243
315
|
}
|
|
316
|
+
var SM2_PUBLIC_KEY_REGEX = /^(?:04)?[0-9a-f]{128}$/i;
|
|
317
|
+
function isValidSM2PublicKey(publicKey) {
|
|
318
|
+
if (typeof publicKey !== "string" || publicKey.length !== 128 && publicKey.length !== 130 || !SM2_PUBLIC_KEY_REGEX.test(publicKey))
|
|
319
|
+
return false;
|
|
320
|
+
const encoded = publicKey.length === 128 ? `04${publicKey}` : publicKey;
|
|
321
|
+
return smCrypto.sm2.verifyPublicKey(encoded) === true;
|
|
322
|
+
}
|
|
244
323
|
|
|
245
324
|
// src/crypto-utils.ts
|
|
246
325
|
function isBase64(str) {
|
|
@@ -264,7 +343,6 @@ function base64ToHex(base64) {
|
|
|
264
343
|
|
|
265
344
|
// src/crypto-sm2.ts
|
|
266
345
|
var { sm2 } = smCrypto;
|
|
267
|
-
var SM2_PUBLIC_KEY_REGEX = /^[0-9a-f]{128}$/i;
|
|
268
346
|
var SM2_PRIVATE_KEY_REGEX = /^[0-9a-f]{64}$/i;
|
|
269
347
|
function createSM2() {
|
|
270
348
|
return {
|
|
@@ -307,7 +385,7 @@ function createSM2() {
|
|
|
307
385
|
);
|
|
308
386
|
}
|
|
309
387
|
try {
|
|
310
|
-
const key = publicKey.
|
|
388
|
+
const key = publicKey.length === 130 ? publicKey : `04${publicKey}`;
|
|
311
389
|
const encrypted = sm2.doEncrypt(data, key, cipherMode);
|
|
312
390
|
if (!encrypted) {
|
|
313
391
|
return err(
|
|
@@ -421,7 +499,7 @@ function createSM2() {
|
|
|
421
499
|
);
|
|
422
500
|
}
|
|
423
501
|
try {
|
|
424
|
-
const key = publicKey.
|
|
502
|
+
const key = publicKey.length === 130 ? publicKey : `04${publicKey}`;
|
|
425
503
|
const isValid = sm2.doVerifySignature(data, signature, key, { hash, userId });
|
|
426
504
|
return ok(!!isValid);
|
|
427
505
|
} catch (error) {
|
|
@@ -433,18 +511,16 @@ function createSM2() {
|
|
|
433
511
|
}
|
|
434
512
|
},
|
|
435
513
|
/**
|
|
436
|
-
*
|
|
514
|
+
* 校验公钥编码及曲线点是否合法
|
|
437
515
|
*
|
|
438
516
|
* 合法格式:128 字符十六进制(无前缀)或 130 字符(含 04 前缀)。
|
|
439
517
|
*
|
|
440
518
|
* @param key - 待校验公钥
|
|
441
|
-
* @returns
|
|
519
|
+
* @returns 编码有效且为 SM2 曲线上的非无穷远点时返回 true
|
|
520
|
+
* @example `crypto.asymmetric.isValidPublicKey(clientPublicKey)`
|
|
442
521
|
*/
|
|
443
522
|
isValidPublicKey(key) {
|
|
444
|
-
|
|
445
|
-
return false;
|
|
446
|
-
const cleanKey = key.startsWith("04") ? key.slice(2) : key;
|
|
447
|
-
return SM2_PUBLIC_KEY_REGEX.test(cleanKey);
|
|
523
|
+
return isValidSM2PublicKey(key);
|
|
448
524
|
},
|
|
449
525
|
/**
|
|
450
526
|
* 校验私钥格式是否合法
|
|
@@ -573,7 +649,7 @@ function stringToBytes(str) {
|
|
|
573
649
|
const encoder = new TextEncoder();
|
|
574
650
|
return Array.from(encoder.encode(str));
|
|
575
651
|
}
|
|
576
|
-
var {
|
|
652
|
+
var { sm4 } = smCrypto;
|
|
577
653
|
var SM4_HEX_32_REGEX = /^[0-9a-f]{32}$/i;
|
|
578
654
|
function createSM4() {
|
|
579
655
|
return {
|
|
@@ -591,7 +667,7 @@ function createSM4() {
|
|
|
591
667
|
* 默认使用 CBC 并自动生成随机 IV,返回结构化字段。
|
|
592
668
|
*
|
|
593
669
|
* ⚠️ 安全警告:ECB 模式会让相同明文块产生相同密文块,泄漏结构信息。
|
|
594
|
-
*
|
|
670
|
+
* 新数据使用默认 CBC,或每次显式传入新的随机 IV;CBC 仍需独立完整性认证,HTTP 使用 transport。
|
|
595
671
|
*
|
|
596
672
|
* @param data - 待加密明文
|
|
597
673
|
* @param key - 密钥(32 字符十六进制)
|
|
@@ -701,7 +777,7 @@ function createSM4() {
|
|
|
701
777
|
*
|
|
702
778
|
* @param data - 待加密明文
|
|
703
779
|
* @param key - 密钥(32 字符十六进制)
|
|
704
|
-
* @returns
|
|
780
|
+
* @returns 成功时返回完整 { mode, ciphertext, iv, encoding };失败时同 encrypt
|
|
705
781
|
*/
|
|
706
782
|
encryptWithIV(data, key) {
|
|
707
783
|
const iv = this.generateIV();
|
|
@@ -719,6 +795,8 @@ function createSM4() {
|
|
|
719
795
|
/**
|
|
720
796
|
* 带 IV 解密(CBC 模式)
|
|
721
797
|
*
|
|
798
|
+
* 这是不含认证的底层 primitive;传输数据必须经 transport 验签及 MAC 后再调用,禁止直接用于 HTTP 解密。
|
|
799
|
+
*
|
|
722
800
|
* @param ciphertext - 密文
|
|
723
801
|
* @param key - 密钥
|
|
724
802
|
* @param iv - 加密时使用的 IV
|
|
@@ -727,26 +805,6 @@ function createSM4() {
|
|
|
727
805
|
decryptWithIV(ciphertext, key, iv) {
|
|
728
806
|
return this.decrypt({ mode: "cbc", ciphertext, iv, encoding: "hex" }, key);
|
|
729
807
|
},
|
|
730
|
-
/**
|
|
731
|
-
* 从密码和盐值派生密钥
|
|
732
|
-
*
|
|
733
|
-
* 内部仅执行单次 SM3 哈希(password + salt),取前 32 字符作为密钥。
|
|
734
|
-
*
|
|
735
|
-
* ⚠️ 安全警告:**此实现不是标准 KDF**,不具备密码爆破抗性(无迭代、无内存硬化)。
|
|
736
|
-
* - **禁止用于密码存储**:密码散列请用 `crypto.password.hash()`。
|
|
737
|
-
* - **禁止用于高价值密钥派生**:如需从密码派生加密密钥,请在应用层自行实现 PBKDF2 / scrypt / Argon2。
|
|
738
|
-
* - 仅适用于测试、迭代兼容或不敏感的场景。
|
|
739
|
-
*
|
|
740
|
-
* @deprecated 未来版本可能移除。密码散列请用 `crypto.password.hash`;密钥派生请使用标准 KDF。
|
|
741
|
-
* @param password - 密码
|
|
742
|
-
* @param salt - 盐值
|
|
743
|
-
* @returns 32 字符十六进制密钥
|
|
744
|
-
*/
|
|
745
|
-
deriveKey(password, salt) {
|
|
746
|
-
const combined = password + salt;
|
|
747
|
-
const hash = sm32(combined);
|
|
748
|
-
return hash.slice(0, 32);
|
|
749
|
-
},
|
|
750
808
|
/** 校验密钥格式是否合法(32 字符十六进制) */
|
|
751
809
|
isValidKey(key) {
|
|
752
810
|
return SM4_HEX_32_REGEX.test(key);
|
|
@@ -769,10 +827,81 @@ function decodeCiphertext(payload) {
|
|
|
769
827
|
return payload.encoding === "base64" ? base64ToHex(payload.ciphertext) : payload.ciphertext;
|
|
770
828
|
}
|
|
771
829
|
|
|
830
|
+
// src/transport/crypto-transport-body.ts
|
|
831
|
+
var TRANSPORT_BODY_LIMITS = {
|
|
832
|
+
/** 密钥协商 JSON 的 UTF-8 总字节数。 */
|
|
833
|
+
keyExchangeBytes: 16384,
|
|
834
|
+
/** GET/HEAD 请求及无正文响应的认证头封装 UTF-8 上限。 */
|
|
835
|
+
envelopeHeaderBytes: 16384,
|
|
836
|
+
/** 单个协商公钥的 UTF-8 字节数。 */
|
|
837
|
+
publicKeyBytes: 4096,
|
|
838
|
+
/** 解密后的请求/加密前的响应明文字节数。 */
|
|
839
|
+
plaintextBytes: 1048576,
|
|
840
|
+
/** 密文 JSON 的 UTF-8 总字节数(2 MiB + 32 KiB,含 CBC 填充与全部认证字段)。 */
|
|
841
|
+
encryptedPayloadBytes: 2129920
|
|
842
|
+
};
|
|
843
|
+
async function readTransportBody(stream, options) {
|
|
844
|
+
if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0)
|
|
845
|
+
throw new RangeError("Invalid transport body byte limit");
|
|
846
|
+
if (!stream) {
|
|
847
|
+
options.signal?.throwIfAborted();
|
|
848
|
+
return "";
|
|
849
|
+
}
|
|
850
|
+
const reader = stream.getReader();
|
|
851
|
+
const signal = options.signal;
|
|
852
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
853
|
+
let completed = false;
|
|
854
|
+
let failure;
|
|
855
|
+
let onAbort;
|
|
856
|
+
try {
|
|
857
|
+
signal?.throwIfAborted();
|
|
858
|
+
const aborted = signal ? new Promise((_resolve, reject) => {
|
|
859
|
+
onAbort = () => reject(signal.reason);
|
|
860
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
861
|
+
}) : void 0;
|
|
862
|
+
let bytes = 0;
|
|
863
|
+
let text = "";
|
|
864
|
+
while (true) {
|
|
865
|
+
const next = reader.read();
|
|
866
|
+
const chunk = await (aborted ? Promise.race([next, aborted]) : next);
|
|
867
|
+
if (chunk.done) {
|
|
868
|
+
completed = true;
|
|
869
|
+
return text + decoder.decode();
|
|
870
|
+
}
|
|
871
|
+
bytes += chunk.value.byteLength;
|
|
872
|
+
if (bytes > options.maxBytes)
|
|
873
|
+
throw new RangeError("Transport body byte limit exceeded");
|
|
874
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
875
|
+
}
|
|
876
|
+
} catch (error) {
|
|
877
|
+
failure = error;
|
|
878
|
+
throw error;
|
|
879
|
+
} finally {
|
|
880
|
+
if (onAbort)
|
|
881
|
+
signal?.removeEventListener("abort", onAbort);
|
|
882
|
+
if (!completed) {
|
|
883
|
+
try {
|
|
884
|
+
void reader.cancel(failure).catch(() => {
|
|
885
|
+
});
|
|
886
|
+
} catch {
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
reader.releaseLock();
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
772
893
|
// src/transport/crypto-transport-types.ts
|
|
773
894
|
var TRANSPORT_PROTOCOL = {
|
|
774
895
|
/** 客户端 ID 请求头名。 */
|
|
775
896
|
CLIENT_ID_HEADER: "X-Client-Id",
|
|
897
|
+
/** GET/HEAD 等无正文请求的认证封装;禁止与正文封装同时出现。 */
|
|
898
|
+
ENVELOPE_HEADER: "X-Hai-Transport",
|
|
899
|
+
/** 唯一支持的认证协议版本;旧未认证格式拒绝。 */
|
|
900
|
+
VERSION: 2,
|
|
901
|
+
/** 请求允许的过去时间窗口(毫秒)。 */
|
|
902
|
+
REQUEST_WINDOW_MS: 3e5,
|
|
903
|
+
/** 节点时钟允许的最大未来偏差(毫秒)。 */
|
|
904
|
+
CLOCK_SKEW_MS: 3e4,
|
|
776
905
|
/** 标识响应体已加密的响应头名。 */
|
|
777
906
|
ENCRYPTED_HEADER: "X-Encrypted",
|
|
778
907
|
/** `X-Encrypted` 响应头的开启值。 */
|
|
@@ -781,11 +910,103 @@ var TRANSPORT_PROTOCOL = {
|
|
|
781
910
|
DEFAULT_KEY_EXCHANGE_PATH: "/_hai/key-exchange"
|
|
782
911
|
};
|
|
783
912
|
|
|
913
|
+
// src/transport/crypto-transport-codec.ts
|
|
914
|
+
function authenticatedText(payload) {
|
|
915
|
+
const c = payload.context;
|
|
916
|
+
return JSON.stringify([
|
|
917
|
+
"hai-transport",
|
|
918
|
+
c.version,
|
|
919
|
+
payload.direction,
|
|
920
|
+
c.serverKeyId,
|
|
921
|
+
c.clientId,
|
|
922
|
+
c.requestId,
|
|
923
|
+
c.issuedAt,
|
|
924
|
+
c.method,
|
|
925
|
+
c.pathHash,
|
|
926
|
+
payload.status,
|
|
927
|
+
payload.encryptedKey,
|
|
928
|
+
payload.ciphertext,
|
|
929
|
+
payload.iv
|
|
930
|
+
]);
|
|
931
|
+
}
|
|
932
|
+
function transportAuthenticationFailure() {
|
|
933
|
+
return err(HaiCryptoError.INVALID_INPUT, cryptoM("crypto_transportAuthenticationFailed"));
|
|
934
|
+
}
|
|
935
|
+
function hashTransportPath(service, path) {
|
|
936
|
+
try {
|
|
937
|
+
if (!path.startsWith("/") || new TextEncoder().encode(path).byteLength > 8192)
|
|
938
|
+
return transportAuthenticationFailure();
|
|
939
|
+
const digest = service.hash.hash(path);
|
|
940
|
+
return digest.success && /^[a-f0-9]{64}$/.test(digest.data) ? digest : transportAuthenticationFailure();
|
|
941
|
+
} catch {
|
|
942
|
+
return transportAuthenticationFailure();
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
function sealTransportPayload(service, plaintext, context, direction, status, recipientPublicKey, senderPrivateKey) {
|
|
946
|
+
try {
|
|
947
|
+
const encryptionKey = service.symmetric.generateKey();
|
|
948
|
+
const authenticationKey = service.symmetric.generateKey() + service.symmetric.generateKey();
|
|
949
|
+
const encrypted = service.symmetric.encryptWithIV(plaintext, encryptionKey);
|
|
950
|
+
const wrapped = service.asymmetric.encrypt(JSON.stringify([encryptionKey, authenticationKey]), recipientPublicKey);
|
|
951
|
+
if (!encrypted.success || !wrapped.success)
|
|
952
|
+
return transportAuthenticationFailure();
|
|
953
|
+
const payload = { context: { ...context }, direction, status, encryptedKey: wrapped.data, ciphertext: encrypted.data.ciphertext, iv: encrypted.data.iv };
|
|
954
|
+
const text = authenticatedText(payload);
|
|
955
|
+
const tag = service.hash.hmac(text, authenticationKey);
|
|
956
|
+
if (!tag.success)
|
|
957
|
+
return transportAuthenticationFailure();
|
|
958
|
+
const signature = service.asymmetric.sign(JSON.stringify([text, tag.data]), senderPrivateKey);
|
|
959
|
+
if (!signature.success)
|
|
960
|
+
return transportAuthenticationFailure();
|
|
961
|
+
const authenticated = { ...payload, tag: tag.data, signature: signature.data };
|
|
962
|
+
return isValidEncryptedPayload(authenticated) ? ok(authenticated) : transportAuthenticationFailure();
|
|
963
|
+
} catch {
|
|
964
|
+
return transportAuthenticationFailure();
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
function openTransportPayload(service, payload, recipientPrivateKey, senderPublicKey) {
|
|
968
|
+
try {
|
|
969
|
+
if (!isValidEncryptedPayload(payload))
|
|
970
|
+
return transportAuthenticationFailure();
|
|
971
|
+
const text = authenticatedText(payload);
|
|
972
|
+
const verified = service.asymmetric.verify(JSON.stringify([text, payload.tag]), payload.signature, senderPublicKey);
|
|
973
|
+
if (!verified.success || !verified.data)
|
|
974
|
+
return transportAuthenticationFailure();
|
|
975
|
+
const unwrapped = service.asymmetric.decrypt(payload.encryptedKey, recipientPrivateKey);
|
|
976
|
+
if (!unwrapped.success)
|
|
977
|
+
return transportAuthenticationFailure();
|
|
978
|
+
const keys = JSON.parse(unwrapped.data);
|
|
979
|
+
if (!Array.isArray(keys) || keys.length !== 2 || typeof keys[0] !== "string" || !/^[a-f0-9]{32}$/.test(keys[0]) || typeof keys[1] !== "string" || !/^[a-f0-9]{64}$/.test(keys[1])) {
|
|
980
|
+
return transportAuthenticationFailure();
|
|
981
|
+
}
|
|
982
|
+
const tag = service.hash.hmac(text, keys[1]);
|
|
983
|
+
if (!tag.success || !core.string.constantTimeEqual(tag.data, payload.tag))
|
|
984
|
+
return transportAuthenticationFailure();
|
|
985
|
+
const plaintext = service.symmetric.decryptWithIV(payload.ciphertext, keys[0], payload.iv);
|
|
986
|
+
if (!plaintext.success)
|
|
987
|
+
return transportAuthenticationFailure();
|
|
988
|
+
return plaintext;
|
|
989
|
+
} catch {
|
|
990
|
+
return transportAuthenticationFailure();
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
function isValidEncryptedPayload(value) {
|
|
994
|
+
if (!value || typeof value !== "object")
|
|
995
|
+
return false;
|
|
996
|
+
const p = value;
|
|
997
|
+
const c = p.context;
|
|
998
|
+
return Object.keys(p).length === 8 && !!c && typeof c === "object" && Object.keys(c).length === 7 && c.version === TRANSPORT_PROTOCOL.VERSION && typeof c.serverKeyId === "string" && /^[\w-]{1,64}$/.test(c.serverKeyId) && typeof c.clientId === "string" && /^[\w-]{1,128}$/.test(c.clientId) && typeof c.requestId === "string" && /^[a-f0-9]{32}$/.test(c.requestId) && Number.isSafeInteger(c.issuedAt) && c.issuedAt > 0 && typeof c.method === "string" && /^[A-Z]{1,16}$/.test(c.method) && typeof c.pathHash === "string" && /^[a-f0-9]{64}$/.test(c.pathHash) && (p.direction === "request" || p.direction === "response") && Number.isInteger(p.status) && (p.direction === "request" ? p.status === 0 : Number(p.status) >= 100 && Number(p.status) <= 599) && typeof p.encryptedKey === "string" && /^[a-f0-9]{1,8192}$/i.test(p.encryptedKey) && typeof p.ciphertext === "string" && p.ciphertext.length <= TRANSPORT_BODY_LIMITS.encryptedPayloadBytes && p.ciphertext.length > 0 && p.ciphertext.length % 32 === 0 && /^[a-f0-9]+$/i.test(p.ciphertext) && typeof p.iv === "string" && /^[a-f0-9]{32}$/i.test(p.iv) && typeof p.tag === "string" && /^[a-f0-9]{64}$/.test(p.tag) && typeof p.signature === "string" && /^[a-f0-9]{128}$/i.test(p.signature);
|
|
999
|
+
}
|
|
1000
|
+
function sameTransportContext(a, b) {
|
|
1001
|
+
return a.version === b.version && a.serverKeyId === b.serverKeyId && a.clientId === b.clientId && a.requestId === b.requestId && a.issuedAt === b.issuedAt && a.method === b.method && a.pathHash === b.pathHash;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
784
1004
|
// src/transport/crypto-transport-client.ts
|
|
785
1005
|
function createTransportClient(options) {
|
|
786
1006
|
const baseFetch = options.fetch ?? fetch;
|
|
787
1007
|
let keyPair = null;
|
|
788
1008
|
let serverPublicKey = null;
|
|
1009
|
+
let serverKeyId = null;
|
|
789
1010
|
let clientId = null;
|
|
790
1011
|
let initPromise = null;
|
|
791
1012
|
async function doInit() {
|
|
@@ -793,30 +1014,47 @@ function createTransportClient(options) {
|
|
|
793
1014
|
if (!kpResult.success)
|
|
794
1015
|
return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportClientKeyGenerateFailed"), kpResult.error);
|
|
795
1016
|
const localKeyPair = kpResult.data;
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
headers: { "Content-Type": "application/json" },
|
|
801
|
-
body: JSON.stringify({ clientPublicKey: localKeyPair.publicKey })
|
|
802
|
-
});
|
|
803
|
-
} catch (cause) {
|
|
804
|
-
return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportKeyExchangeRequestFailed"), cause);
|
|
805
|
-
}
|
|
806
|
-
if (!response.ok)
|
|
807
|
-
return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportKeyExchangeHttpFailed", { params: { status: response.status } }));
|
|
808
|
-
let body;
|
|
1017
|
+
if (new TextEncoder().encode(localKeyPair.publicKey).byteLength > TRANSPORT_BODY_LIMITS.publicKeyBytes)
|
|
1018
|
+
return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportClientKeyGenerateFailed"));
|
|
1019
|
+
const controller = new AbortController();
|
|
1020
|
+
const timeout = setTimeout(() => controller.abort(new DOMException("Key exchange timed out", "TimeoutError")), 3e4);
|
|
809
1021
|
try {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
1022
|
+
let response;
|
|
1023
|
+
try {
|
|
1024
|
+
response = await baseFetch(options.keyExchangeUrl, {
|
|
1025
|
+
method: "POST",
|
|
1026
|
+
headers: { "Content-Type": "application/json" },
|
|
1027
|
+
body: JSON.stringify({ clientPublicKey: localKeyPair.publicKey }),
|
|
1028
|
+
signal: controller.signal
|
|
1029
|
+
});
|
|
1030
|
+
} catch (cause) {
|
|
1031
|
+
return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportKeyExchangeRequestFailed"), cause);
|
|
1032
|
+
}
|
|
1033
|
+
if (!response.ok) {
|
|
1034
|
+
void response.body?.cancel().catch(() => {
|
|
1035
|
+
});
|
|
1036
|
+
return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportKeyExchangeHttpFailed", { params: { status: response.status } }));
|
|
1037
|
+
}
|
|
1038
|
+
let body;
|
|
1039
|
+
try {
|
|
1040
|
+
body = JSON.parse(await readTransportBody(response.body, {
|
|
1041
|
+
maxBytes: TRANSPORT_BODY_LIMITS.keyExchangeBytes,
|
|
1042
|
+
signal: controller.signal
|
|
1043
|
+
}));
|
|
1044
|
+
} catch {
|
|
1045
|
+
return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportKeyExchangeInvalidJson"));
|
|
1046
|
+
}
|
|
1047
|
+
if (!isKeyExchangeResponse(body)) {
|
|
1048
|
+
return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportKeyExchangeMissingFields"));
|
|
1049
|
+
}
|
|
1050
|
+
keyPair = localKeyPair;
|
|
1051
|
+
serverPublicKey = body.serverPublicKey;
|
|
1052
|
+
serverKeyId = body.serverKeyId;
|
|
1053
|
+
clientId = body.clientId;
|
|
1054
|
+
return ok(void 0);
|
|
1055
|
+
} finally {
|
|
1056
|
+
clearTimeout(timeout);
|
|
813
1057
|
}
|
|
814
|
-
if (!body.serverPublicKey || !body.clientId)
|
|
815
|
-
return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportKeyExchangeMissingFields"));
|
|
816
|
-
keyPair = localKeyPair;
|
|
817
|
-
serverPublicKey = body.serverPublicKey;
|
|
818
|
-
clientId = body.clientId;
|
|
819
|
-
return ok(void 0);
|
|
820
1058
|
}
|
|
821
1059
|
async function ensureReady() {
|
|
822
1060
|
if (clientId && serverPublicKey)
|
|
@@ -830,66 +1068,91 @@ function createTransportClient(options) {
|
|
|
830
1068
|
}
|
|
831
1069
|
return initPromise;
|
|
832
1070
|
}
|
|
833
|
-
function encryptBody(plaintext) {
|
|
834
|
-
if (!serverPublicKey)
|
|
1071
|
+
function encryptBody(plaintext, context) {
|
|
1072
|
+
if (!serverPublicKey || !serverKeyId || !keyPair)
|
|
835
1073
|
return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportClientNotInitialized"));
|
|
836
|
-
|
|
837
|
-
const encResult = options.crypto.symmetric.encryptWithIV(plaintext, symKey);
|
|
838
|
-
if (!encResult.success)
|
|
839
|
-
return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportRequestEncryptFailed"), encResult.error);
|
|
840
|
-
const keyEncResult = options.crypto.asymmetric.encrypt(symKey, serverPublicKey);
|
|
841
|
-
if (!keyEncResult.success)
|
|
842
|
-
return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportSessionKeyEncryptFailed"), keyEncResult.error);
|
|
843
|
-
return ok({
|
|
844
|
-
encryptedKey: keyEncResult.data,
|
|
845
|
-
ciphertext: encResult.data.ciphertext,
|
|
846
|
-
iv: encResult.data.iv
|
|
847
|
-
});
|
|
1074
|
+
return sealTransportPayload(options.crypto, plaintext, context, "request", 0, serverPublicKey, keyPair.privateKey);
|
|
848
1075
|
}
|
|
849
1076
|
function decryptPayload(payload) {
|
|
850
|
-
if (!keyPair)
|
|
1077
|
+
if (!keyPair || !serverPublicKey)
|
|
851
1078
|
return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportClientNotInitialized"));
|
|
852
|
-
|
|
853
|
-
if (!keyDec.success)
|
|
854
|
-
return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportSessionKeyDecryptFailed"), keyDec.error);
|
|
855
|
-
const dec = options.crypto.symmetric.decryptWithIV(payload.ciphertext, keyDec.data, payload.iv);
|
|
856
|
-
if (!dec.success)
|
|
857
|
-
return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportResponseDecryptFailed"), dec.error);
|
|
858
|
-
return ok(dec.data);
|
|
1079
|
+
return openTransportPayload(options.crypto, payload, keyPair.privateKey, serverPublicKey);
|
|
859
1080
|
}
|
|
860
1081
|
const encryptedFetch = async (input, init) => {
|
|
861
1082
|
const requestInput = isRequestLike(input) ? input : void 0;
|
|
1083
|
+
const signal = init?.signal ?? requestInput?.signal ?? void 0;
|
|
1084
|
+
signal?.throwIfAborted();
|
|
862
1085
|
const targetUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : requestInput?.url ?? String(input);
|
|
863
1086
|
if (targetUrl === options.keyExchangeUrl)
|
|
864
1087
|
return baseFetch(input, init);
|
|
865
|
-
|
|
1088
|
+
let onAbort;
|
|
1089
|
+
let ready;
|
|
1090
|
+
try {
|
|
1091
|
+
ready = await (signal ? Promise.race([
|
|
1092
|
+
ensureReady(),
|
|
1093
|
+
new Promise((_resolve, reject) => {
|
|
1094
|
+
onAbort = () => reject(signal.reason);
|
|
1095
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1096
|
+
if (signal.aborted)
|
|
1097
|
+
onAbort();
|
|
1098
|
+
})
|
|
1099
|
+
]) : ensureReady());
|
|
1100
|
+
} finally {
|
|
1101
|
+
if (onAbort)
|
|
1102
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1103
|
+
}
|
|
866
1104
|
if (!ready.success)
|
|
867
1105
|
throw new Error(ready.error.message);
|
|
868
1106
|
const headers = new Headers(init?.headers ?? requestInput?.headers);
|
|
869
1107
|
headers.set(TRANSPORT_PROTOCOL.CLIENT_ID_HEADER, clientId);
|
|
870
1108
|
let rawBody = init?.body;
|
|
871
1109
|
if (rawBody == null && requestInput) {
|
|
872
|
-
const
|
|
873
|
-
if (
|
|
874
|
-
const text = await requestInput.
|
|
1110
|
+
const method2 = (init?.method ?? requestInput.method).toUpperCase();
|
|
1111
|
+
if (method2 !== "GET" && method2 !== "HEAD") {
|
|
1112
|
+
const text = await readTransportBody(requestInput.body, { maxBytes: TRANSPORT_BODY_LIMITS.plaintextBytes, signal });
|
|
875
1113
|
if (text)
|
|
876
1114
|
rawBody = text;
|
|
877
1115
|
}
|
|
878
1116
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1117
|
+
const method = (init?.method ?? requestInput?.method ?? "GET").toUpperCase();
|
|
1118
|
+
const base = new URL(options.keyExchangeUrl, globalThis.location?.href ?? "http://localhost");
|
|
1119
|
+
const url = new URL(targetUrl, base);
|
|
1120
|
+
const pathHash = hashTransportPath(options.crypto, url.pathname + url.search);
|
|
1121
|
+
if (!pathHash.success)
|
|
1122
|
+
throw new Error(pathHash.error.message);
|
|
1123
|
+
const context = {
|
|
1124
|
+
version: TRANSPORT_PROTOCOL.VERSION,
|
|
1125
|
+
serverKeyId,
|
|
1126
|
+
clientId,
|
|
1127
|
+
requestId: options.crypto.symmetric.generateKey(),
|
|
1128
|
+
issuedAt: Date.now(),
|
|
1129
|
+
method,
|
|
1130
|
+
pathHash: pathHash.data
|
|
1131
|
+
};
|
|
1132
|
+
const plaintext = rawBody == null ? "" : await readTransportBody(new Response(rawBody).body, { maxBytes: TRANSPORT_BODY_LIMITS.plaintextBytes, signal });
|
|
1133
|
+
const encResult = encryptBody(plaintext, context);
|
|
1134
|
+
if (!encResult.success)
|
|
1135
|
+
throw new Error(encResult.error.message);
|
|
1136
|
+
const serialized = JSON.stringify(encResult.data);
|
|
1137
|
+
if (new TextEncoder().encode(serialized).byteLength > TRANSPORT_BODY_LIMITS.encryptedPayloadBytes)
|
|
1138
|
+
throw new RangeError("Transport request envelope byte limit exceeded");
|
|
1139
|
+
let nextBody;
|
|
1140
|
+
headers.delete(TRANSPORT_PROTOCOL.ENVELOPE_HEADER);
|
|
1141
|
+
headers.set(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER, TRANSPORT_PROTOCOL.ENCRYPTED_HEADER_VALUE);
|
|
1142
|
+
headers.delete("Content-Length");
|
|
1143
|
+
headers.delete("Content-Encoding");
|
|
1144
|
+
if (method === "GET" || method === "HEAD") {
|
|
1145
|
+
if (new TextEncoder().encode(serialized).byteLength > TRANSPORT_BODY_LIMITS.envelopeHeaderBytes)
|
|
1146
|
+
throw new RangeError("Transport request envelope header byte limit exceeded");
|
|
1147
|
+
headers.set(TRANSPORT_PROTOCOL.ENVELOPE_HEADER, serialized);
|
|
1148
|
+
} else {
|
|
1149
|
+
nextBody = serialized;
|
|
886
1150
|
headers.set("Content-Type", "application/json");
|
|
887
|
-
headers.set(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER, TRANSPORT_PROTOCOL.ENCRYPTED_HEADER_VALUE);
|
|
888
1151
|
}
|
|
889
1152
|
const finalInput = requestInput ? requestInput.url : input;
|
|
890
1153
|
const finalInit = {
|
|
891
1154
|
...init,
|
|
892
|
-
method
|
|
1155
|
+
method,
|
|
893
1156
|
headers,
|
|
894
1157
|
body: nextBody,
|
|
895
1158
|
cache: init?.cache ?? requestInput?.cache,
|
|
@@ -903,24 +1166,39 @@ function createTransportClient(options) {
|
|
|
903
1166
|
signal: init?.signal ?? requestInput?.signal
|
|
904
1167
|
};
|
|
905
1168
|
const response = await baseFetch(finalInput, finalInit);
|
|
906
|
-
if (response.headers.get(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER) !== TRANSPORT_PROTOCOL.ENCRYPTED_HEADER_VALUE)
|
|
907
|
-
|
|
908
|
-
|
|
1169
|
+
if (response.headers.get(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER) !== TRANSPORT_PROTOCOL.ENCRYPTED_HEADER_VALUE) {
|
|
1170
|
+
void response.body?.cancel().catch(() => {
|
|
1171
|
+
});
|
|
1172
|
+
throw new Error(cryptoM("crypto_transportResponseDecryptFailed"));
|
|
1173
|
+
}
|
|
909
1174
|
let payload;
|
|
910
1175
|
try {
|
|
911
|
-
|
|
1176
|
+
const envelope = response.headers.get(TRANSPORT_PROTOCOL.ENVELOPE_HEADER);
|
|
1177
|
+
if (envelope && response.body !== null) {
|
|
1178
|
+
void response.body.cancel().catch(() => {
|
|
1179
|
+
});
|
|
1180
|
+
throw new Error(cryptoM("crypto_transportResponseDecryptFailed"));
|
|
1181
|
+
}
|
|
1182
|
+
if (envelope && new TextEncoder().encode(envelope).byteLength > TRANSPORT_BODY_LIMITS.envelopeHeaderBytes)
|
|
1183
|
+
throw new RangeError("Transport response envelope byte limit exceeded");
|
|
1184
|
+
payload = JSON.parse(envelope ?? await readTransportBody(response.body, { maxBytes: TRANSPORT_BODY_LIMITS.encryptedPayloadBytes, signal }));
|
|
912
1185
|
} catch {
|
|
913
|
-
|
|
1186
|
+
throw new Error(cryptoM("crypto_transportResponseDecryptFailed"), signal?.aborted ? { cause: signal.reason } : void 0);
|
|
1187
|
+
}
|
|
1188
|
+
if (!isValidEncryptedPayload(payload) || payload.direction !== "response" || payload.status !== response.status || !sameTransportContext(payload.context, context)) {
|
|
1189
|
+
throw new Error(cryptoM("crypto_transportResponseDecryptFailed"));
|
|
914
1190
|
}
|
|
915
|
-
if (!isEncryptedPayload(payload))
|
|
916
|
-
return response;
|
|
917
1191
|
const decResult = decryptPayload(payload);
|
|
918
1192
|
if (!decResult.success)
|
|
919
1193
|
throw new Error(decResult.error.message);
|
|
1194
|
+
if (new TextEncoder().encode(decResult.data).byteLength > TRANSPORT_BODY_LIMITS.plaintextBytes)
|
|
1195
|
+
throw new RangeError("Transport response plaintext byte limit exceeded");
|
|
920
1196
|
const respHeaders = new Headers(response.headers);
|
|
921
1197
|
respHeaders.delete(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER);
|
|
1198
|
+
respHeaders.delete(TRANSPORT_PROTOCOL.ENVELOPE_HEADER);
|
|
922
1199
|
respHeaders.delete("Content-Length");
|
|
923
|
-
|
|
1200
|
+
respHeaders.delete("Content-Encoding");
|
|
1201
|
+
return new Response(method === "HEAD" || [204, 304].includes(response.status) ? null : decResult.data, {
|
|
924
1202
|
status: response.status,
|
|
925
1203
|
statusText: response.statusText,
|
|
926
1204
|
headers: respHeaders
|
|
@@ -933,24 +1211,12 @@ function createTransportClient(options) {
|
|
|
933
1211
|
destroy() {
|
|
934
1212
|
keyPair = null;
|
|
935
1213
|
serverPublicKey = null;
|
|
1214
|
+
serverKeyId = null;
|
|
936
1215
|
clientId = null;
|
|
937
1216
|
initPromise = null;
|
|
938
1217
|
}
|
|
939
1218
|
};
|
|
940
1219
|
}
|
|
941
|
-
async function bodyToText(body) {
|
|
942
|
-
if (typeof body === "string")
|
|
943
|
-
return body;
|
|
944
|
-
if (body instanceof URLSearchParams)
|
|
945
|
-
return body.toString();
|
|
946
|
-
if (body instanceof Blob)
|
|
947
|
-
return body.text();
|
|
948
|
-
if (body instanceof ArrayBuffer)
|
|
949
|
-
return new TextDecoder().decode(body);
|
|
950
|
-
if (ArrayBuffer.isView(body))
|
|
951
|
-
return new TextDecoder().decode(body.buffer);
|
|
952
|
-
return new Response(body).text();
|
|
953
|
-
}
|
|
954
1220
|
function isRequestLike(input) {
|
|
955
1221
|
if (typeof input !== "object" || input === null)
|
|
956
1222
|
return false;
|
|
@@ -958,95 +1224,119 @@ function isRequestLike(input) {
|
|
|
958
1224
|
const headers = candidate.headers;
|
|
959
1225
|
return typeof candidate.url === "string" && typeof headers?.get === "function" && typeof candidate.method === "string" && typeof candidate.clone === "function";
|
|
960
1226
|
}
|
|
961
|
-
function
|
|
962
|
-
if (!
|
|
1227
|
+
function isKeyExchangeResponse(body) {
|
|
1228
|
+
if (!body || typeof body !== "object")
|
|
963
1229
|
return false;
|
|
964
|
-
const
|
|
965
|
-
return typeof
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
// src/transport/store-provider/crypto-transport-store-memory.ts
|
|
969
|
-
var DEFAULT_MAX_CLIENTS = 1e4;
|
|
970
|
-
function normalizeMaxClients(maxClients) {
|
|
971
|
-
if (!Number.isFinite(maxClients))
|
|
972
|
-
return DEFAULT_MAX_CLIENTS;
|
|
973
|
-
return Math.max(1, Math.floor(maxClients));
|
|
1230
|
+
const fields = body;
|
|
1231
|
+
return typeof fields.serverPublicKey === "string" && fields.serverPublicKey.length > 0 && new TextEncoder().encode(fields.serverPublicKey).byteLength <= TRANSPORT_BODY_LIMITS.publicKeyBytes && typeof fields.clientId === "string" && fields.clientId.length > 0 && typeof fields.serverKeyId === "string" && /^[\w-]{1,64}$/.test(fields.serverKeyId);
|
|
974
1232
|
}
|
|
975
|
-
function
|
|
976
|
-
|
|
977
|
-
|
|
1233
|
+
function parseTransportServerKeyRing(serialized) {
|
|
1234
|
+
try {
|
|
1235
|
+
const value = typeof serialized === "string" ? JSON.parse(serialized) : serialized;
|
|
1236
|
+
if (isTransportServerKeyRing(value))
|
|
1237
|
+
return ok(value);
|
|
1238
|
+
} catch {
|
|
1239
|
+
}
|
|
1240
|
+
return err(HaiCryptoError.INVALID_KEY, cryptoM("crypto_transportServerKeyRingInvalid"));
|
|
978
1241
|
}
|
|
979
|
-
function
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
},
|
|
995
|
-
async get(clientId) {
|
|
996
|
-
return clientKeys.get(clientId);
|
|
997
|
-
},
|
|
998
|
-
async delete(clientId) {
|
|
999
|
-
clientKeys.delete(clientId);
|
|
1000
|
-
},
|
|
1001
|
-
async close() {
|
|
1002
|
-
clientKeys.clear();
|
|
1003
|
-
}
|
|
1004
|
-
};
|
|
1242
|
+
function isTransportServerKeyRing(value) {
|
|
1243
|
+
if (!value || typeof value !== "object")
|
|
1244
|
+
return false;
|
|
1245
|
+
const ring = value;
|
|
1246
|
+
if (typeof ring.activeKeyId !== "string" || !/^[\w-]{1,64}$/.test(ring.activeKeyId) || !ring.keys || typeof ring.keys !== "object" || Array.isArray(ring.keys)) {
|
|
1247
|
+
return false;
|
|
1248
|
+
}
|
|
1249
|
+
if (!Object.hasOwn(ring.keys, ring.activeKeyId))
|
|
1250
|
+
return false;
|
|
1251
|
+
return Object.entries(ring.keys).every(([id, pair]) => {
|
|
1252
|
+
if (!/^[\w-]{1,64}$/.test(id) || !pair || typeof pair !== "object")
|
|
1253
|
+
return false;
|
|
1254
|
+
const fields = pair;
|
|
1255
|
+
return typeof fields.publicKey === "string" && fields.publicKey.length > 0 && new TextEncoder().encode(fields.publicKey).byteLength <= TRANSPORT_BODY_LIMITS.publicKeyBytes && typeof fields.privateKey === "string" && fields.privateKey.length > 0 && fields.privateKey.length <= TRANSPORT_BODY_LIMITS.publicKeyBytes;
|
|
1256
|
+
});
|
|
1005
1257
|
}
|
|
1006
1258
|
|
|
1007
1259
|
// src/transport/crypto-transport-server.ts
|
|
1008
|
-
function createTransportEncryption(cryptoService, options
|
|
1009
|
-
const
|
|
1010
|
-
if (!
|
|
1011
|
-
return
|
|
1012
|
-
|
|
1013
|
-
|
|
1260
|
+
function createTransportEncryption(cryptoService, options) {
|
|
1261
|
+
const ring = parseTransportServerKeyRing(options?.serverKeyRing);
|
|
1262
|
+
if (!ring.success)
|
|
1263
|
+
return ring;
|
|
1264
|
+
if (!options.keyStore || typeof options.keyStore.register !== "function" || typeof options.keyStore.get !== "function" || typeof options.keyStore.claimRequest !== "function") {
|
|
1265
|
+
return err(HaiCryptoError.INVALID_INPUT, cryptoM("crypto_transportKeyStoreRequired"));
|
|
1266
|
+
}
|
|
1267
|
+
if (core.module.requiresSharedState(globalThis.process?.env ?? {}) && options.keyStore.stateScope !== "shared")
|
|
1268
|
+
return err(HaiCryptoError.INVALID_INPUT, cryptoM("crypto_transportKeyStoreRequired"));
|
|
1269
|
+
const serverKeys = /* @__PURE__ */ new Map();
|
|
1270
|
+
for (const [id, key] of Object.entries(ring.data.keys)) {
|
|
1271
|
+
const probe = cryptoService.asymmetric.encrypt("hai-transport-key-check", key.publicKey);
|
|
1272
|
+
if (!probe.success)
|
|
1273
|
+
return err(HaiCryptoError.INVALID_KEY, cryptoM("crypto_transportServerKeyRingInvalid"));
|
|
1274
|
+
const checked = cryptoService.asymmetric.decrypt(probe.data, key.privateKey);
|
|
1275
|
+
if (!checked.success || checked.data !== "hai-transport-key-check")
|
|
1276
|
+
return err(HaiCryptoError.INVALID_KEY, cryptoM("crypto_transportServerKeyRingInvalid"));
|
|
1277
|
+
serverKeys.set(id, { publicKey: key.publicKey, privateKey: key.privateKey });
|
|
1278
|
+
}
|
|
1279
|
+
const activeKeyId = ring.data.activeKeyId;
|
|
1280
|
+
const serverKeyPair = serverKeys.get(activeKeyId);
|
|
1281
|
+
const keyStore = options.keyStore;
|
|
1014
1282
|
const manager = {
|
|
1283
|
+
getServerKeyId() {
|
|
1284
|
+
return activeKeyId;
|
|
1285
|
+
},
|
|
1015
1286
|
getServerPublicKey() {
|
|
1016
1287
|
return serverKeyPair.publicKey;
|
|
1017
1288
|
},
|
|
1018
1289
|
async registerClientKey(clientPublicKey) {
|
|
1290
|
+
if (!isValidSM2PublicKey(clientPublicKey))
|
|
1291
|
+
throw new TypeError(cryptoM("crypto_sm2PublicKeyInvalid"));
|
|
1019
1292
|
return keyStore.register(clientPublicKey);
|
|
1020
1293
|
},
|
|
1021
1294
|
async getClientPublicKey(clientId) {
|
|
1022
1295
|
return keyStore.get(clientId);
|
|
1023
1296
|
},
|
|
1024
|
-
async encryptResponse(
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
ciphertext: encResult.data.ciphertext,
|
|
1038
|
-
iv: encResult.data.iv
|
|
1039
|
-
};
|
|
1040
|
-
return ok(payload);
|
|
1297
|
+
async encryptResponse(request, data, status) {
|
|
1298
|
+
try {
|
|
1299
|
+
const context = { ...request };
|
|
1300
|
+
const clientPublicKey = await keyStore.get(context.clientId);
|
|
1301
|
+
if (!clientPublicKey)
|
|
1302
|
+
return err(HaiCommonError.NOT_FOUND, cryptoM("crypto_transportClientNotRegistered"));
|
|
1303
|
+
const key = serverKeys.get(context.serverKeyId);
|
|
1304
|
+
if (!key)
|
|
1305
|
+
return transportAuthenticationFailure();
|
|
1306
|
+
return sealTransportPayload(cryptoService, data, context, "response", status, clientPublicKey, key.privateKey);
|
|
1307
|
+
} catch {
|
|
1308
|
+
return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportResponseEncryptFailed"));
|
|
1309
|
+
}
|
|
1041
1310
|
},
|
|
1042
|
-
decryptRequest(payload) {
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1311
|
+
async decryptRequest(payload, expected) {
|
|
1312
|
+
try {
|
|
1313
|
+
if (!isValidEncryptedPayload(payload) || payload.direction !== "request" || payload.status !== 0)
|
|
1314
|
+
return transportAuthenticationFailure();
|
|
1315
|
+
const authenticated = { ...payload, context: { ...payload.context } };
|
|
1316
|
+
const c = authenticated.context;
|
|
1317
|
+
const now = Date.now();
|
|
1318
|
+
const pathHash = hashTransportPath(cryptoService, expected.path);
|
|
1319
|
+
if (!pathHash.success || c.clientId !== expected.clientId || c.method !== expected.method || c.pathHash !== pathHash.data || c.issuedAt > now + TRANSPORT_PROTOCOL.CLOCK_SKEW_MS || now >= c.issuedAt + TRANSPORT_PROTOCOL.REQUEST_WINDOW_MS) {
|
|
1320
|
+
return transportAuthenticationFailure();
|
|
1321
|
+
}
|
|
1322
|
+
const key = serverKeys.get(c.serverKeyId);
|
|
1323
|
+
const clientPublicKey = await keyStore.get(c.clientId);
|
|
1324
|
+
if (!key || !clientPublicKey)
|
|
1325
|
+
return transportAuthenticationFailure();
|
|
1326
|
+
const plaintext = openTransportPayload(cryptoService, authenticated, key.privateKey, clientPublicKey);
|
|
1327
|
+
if (!plaintext.success)
|
|
1328
|
+
return plaintext;
|
|
1329
|
+
if (Date.now() >= c.issuedAt + TRANSPORT_PROTOCOL.REQUEST_WINDOW_MS)
|
|
1330
|
+
return transportAuthenticationFailure();
|
|
1331
|
+
if (!await keyStore.claimRequest(c.clientId, c.requestId, c.issuedAt + TRANSPORT_PROTOCOL.REQUEST_WINDOW_MS + 2 * TRANSPORT_PROTOCOL.CLOCK_SKEW_MS)) {
|
|
1332
|
+
return transportAuthenticationFailure();
|
|
1333
|
+
}
|
|
1334
|
+
if (Date.now() >= c.issuedAt + TRANSPORT_PROTOCOL.REQUEST_WINDOW_MS)
|
|
1335
|
+
return transportAuthenticationFailure();
|
|
1336
|
+
return plaintext;
|
|
1337
|
+
} catch {
|
|
1338
|
+
return transportAuthenticationFailure();
|
|
1339
|
+
}
|
|
1050
1340
|
},
|
|
1051
1341
|
async close() {
|
|
1052
1342
|
await keyStore.close?.();
|
|
@@ -1075,7 +1365,7 @@ function getCrypto() {
|
|
|
1075
1365
|
return crypto2;
|
|
1076
1366
|
}
|
|
1077
1367
|
var transportOperations = {
|
|
1078
|
-
createServer: (options) => createTransportEncryption(getCrypto(), options),
|
|
1368
|
+
createServer: (options) => getCrypto().isInitialized ? createTransportEncryption(getCrypto(), options) : notInitialized.result(),
|
|
1079
1369
|
createClient: (options) => createTransportClient({ crypto: getCrypto(), ...options }),
|
|
1080
1370
|
protocol: TRANSPORT_PROTOCOL
|
|
1081
1371
|
};
|
|
@@ -1148,8 +1438,7 @@ var crypto2 = {
|
|
|
1148
1438
|
/**
|
|
1149
1439
|
* 传输加密操作(端到端混合加密)
|
|
1150
1440
|
*
|
|
1151
|
-
* createServer
|
|
1152
|
-
* 自动通过 asymmetric proxy 返回 NOT_INITIALIZED 错误。
|
|
1441
|
+
* createServer 检查初始化后复用 asymmetric/symmetric;未初始化返回 NOT_INITIALIZED。
|
|
1153
1442
|
*/
|
|
1154
1443
|
transport: transportOperations,
|
|
1155
1444
|
/** 是否已初始化 */
|
|
@@ -1175,13 +1464,116 @@ var crypto2 = {
|
|
|
1175
1464
|
logger.info("Crypto module closed");
|
|
1176
1465
|
}
|
|
1177
1466
|
};
|
|
1467
|
+
async function authenticateTransportRequest(manager, request) {
|
|
1468
|
+
const reject = () => {
|
|
1469
|
+
void request.body?.cancel().catch(() => {
|
|
1470
|
+
});
|
|
1471
|
+
return transportAuthenticationFailure();
|
|
1472
|
+
};
|
|
1473
|
+
if (request.headers.get(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER) !== TRANSPORT_PROTOCOL.ENCRYPTED_HEADER_VALUE)
|
|
1474
|
+
return reject();
|
|
1475
|
+
const clientId = request.headers.get(TRANSPORT_PROTOCOL.CLIENT_ID_HEADER);
|
|
1476
|
+
if (!clientId)
|
|
1477
|
+
return reject();
|
|
1478
|
+
const envelope = request.headers.get(TRANSPORT_PROTOCOL.ENVELOPE_HEADER);
|
|
1479
|
+
const bodyless = request.method === "GET" || request.method === "HEAD";
|
|
1480
|
+
if (bodyless && (!envelope || request.body !== null) || !bodyless && envelope !== null)
|
|
1481
|
+
return reject();
|
|
1482
|
+
if (envelope && new TextEncoder().encode(envelope).byteLength > TRANSPORT_BODY_LIMITS.envelopeHeaderBytes)
|
|
1483
|
+
throw new RangeError("Transport request envelope byte limit exceeded");
|
|
1484
|
+
const text = envelope ?? await readTransportBody(request.body, {
|
|
1485
|
+
maxBytes: TRANSPORT_BODY_LIMITS.encryptedPayloadBytes,
|
|
1486
|
+
signal: request.signal
|
|
1487
|
+
});
|
|
1488
|
+
let payload;
|
|
1489
|
+
try {
|
|
1490
|
+
payload = JSON.parse(text);
|
|
1491
|
+
} catch {
|
|
1492
|
+
return transportAuthenticationFailure();
|
|
1493
|
+
}
|
|
1494
|
+
if (!isValidEncryptedPayload(payload) || payload.direction !== "request" || payload.status !== 0)
|
|
1495
|
+
return transportAuthenticationFailure();
|
|
1496
|
+
const url = new URL(request.url);
|
|
1497
|
+
const decrypted = await manager.decryptRequest(payload, {
|
|
1498
|
+
clientId,
|
|
1499
|
+
method: request.method,
|
|
1500
|
+
path: url.pathname + url.search
|
|
1501
|
+
});
|
|
1502
|
+
if (!decrypted.success)
|
|
1503
|
+
return decrypted;
|
|
1504
|
+
if (new TextEncoder().encode(decrypted.data).byteLength > TRANSPORT_BODY_LIMITS.plaintextBytes)
|
|
1505
|
+
throw new RangeError("Transport request plaintext byte limit exceeded");
|
|
1506
|
+
if (bodyless && decrypted.data !== "")
|
|
1507
|
+
return transportAuthenticationFailure();
|
|
1508
|
+
return ok({ plaintext: decrypted.data, context: payload.context });
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// src/transport/store-provider/crypto-transport-store-memory.ts
|
|
1512
|
+
var DEFAULT_MAX_CLIENTS = 1e4;
|
|
1513
|
+
function normalizeMaxClients(maxClients) {
|
|
1514
|
+
if (!Number.isFinite(maxClients))
|
|
1515
|
+
return DEFAULT_MAX_CLIENTS;
|
|
1516
|
+
return Math.max(1, Math.floor(maxClients));
|
|
1517
|
+
}
|
|
1518
|
+
function createClientId(counter) {
|
|
1519
|
+
const entropy = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID().replace(/-/g, "") : Math.random().toString(36).slice(2, 12);
|
|
1520
|
+
return `c_${counter}_${Date.now()}_${entropy}`;
|
|
1521
|
+
}
|
|
1522
|
+
function createInMemoryKeyStore(maxClients = DEFAULT_MAX_CLIENTS) {
|
|
1523
|
+
const clientKeys = /* @__PURE__ */ new Map();
|
|
1524
|
+
const requests = /* @__PURE__ */ new Map();
|
|
1525
|
+
const capacity = normalizeMaxClients(maxClients);
|
|
1526
|
+
let counter = 0;
|
|
1527
|
+
return {
|
|
1528
|
+
get stateScope() {
|
|
1529
|
+
return "local";
|
|
1530
|
+
},
|
|
1531
|
+
async register(publicKey) {
|
|
1532
|
+
if (!isValidSM2PublicKey(publicKey))
|
|
1533
|
+
throw new TypeError(cryptoM("crypto_sm2PublicKeyInvalid"));
|
|
1534
|
+
counter++;
|
|
1535
|
+
const clientId = createClientId(counter);
|
|
1536
|
+
if (clientKeys.size >= capacity) {
|
|
1537
|
+
const oldest = clientKeys.keys().next().value;
|
|
1538
|
+
if (oldest !== void 0)
|
|
1539
|
+
clientKeys.delete(oldest);
|
|
1540
|
+
}
|
|
1541
|
+
clientKeys.set(clientId, publicKey);
|
|
1542
|
+
return clientId;
|
|
1543
|
+
},
|
|
1544
|
+
async get(clientId) {
|
|
1545
|
+
return clientKeys.get(clientId);
|
|
1546
|
+
},
|
|
1547
|
+
async claimRequest(clientId, requestId, expiresAt) {
|
|
1548
|
+
const now = Date.now();
|
|
1549
|
+
for (const [id2, expiry] of requests) {
|
|
1550
|
+
if (expiry <= now)
|
|
1551
|
+
requests.delete(id2);
|
|
1552
|
+
}
|
|
1553
|
+
const id = JSON.stringify([clientId, requestId]);
|
|
1554
|
+
if (requests.has(id) || expiresAt <= now)
|
|
1555
|
+
return false;
|
|
1556
|
+
requests.set(id, expiresAt);
|
|
1557
|
+
return true;
|
|
1558
|
+
},
|
|
1559
|
+
async delete(clientId) {
|
|
1560
|
+
clientKeys.delete(clientId);
|
|
1561
|
+
},
|
|
1562
|
+
async close() {
|
|
1563
|
+
clientKeys.clear();
|
|
1564
|
+
requests.clear();
|
|
1565
|
+
}
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1178
1568
|
|
|
1179
1569
|
// src/transport/store-provider/crypto-transport-store-redis.ts
|
|
1180
1570
|
var TRANSPORT_KEY_CACHE_PREFIX = "hai:crypto:transport:client";
|
|
1181
1571
|
function normalizeTtlSeconds(ttlSeconds) {
|
|
1182
|
-
if (ttlSeconds === void 0
|
|
1572
|
+
if (ttlSeconds === void 0)
|
|
1183
1573
|
return void 0;
|
|
1184
|
-
|
|
1574
|
+
if (!Number.isSafeInteger(ttlSeconds) || ttlSeconds <= 0 || ttlSeconds > 2147483647)
|
|
1575
|
+
throw new TypeError(cryptoM("crypto_transportKeyStoreTtlInvalid"));
|
|
1576
|
+
return ttlSeconds;
|
|
1185
1577
|
}
|
|
1186
1578
|
function createClientId2() {
|
|
1187
1579
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function")
|
|
@@ -1204,9 +1596,14 @@ function createRedisTransportKeyStore(options) {
|
|
|
1204
1596
|
return `${TRANSPORT_KEY_CACHE_PREFIX}:${clientId}`;
|
|
1205
1597
|
}
|
|
1206
1598
|
return {
|
|
1599
|
+
get stateScope() {
|
|
1600
|
+
return options.cache.config?.type === "redis" ? "shared" : "local";
|
|
1601
|
+
},
|
|
1207
1602
|
async register(publicKey) {
|
|
1603
|
+
if (!isValidSM2PublicKey(publicKey))
|
|
1604
|
+
throw new TypeError(cryptoM("crypto_sm2PublicKeyInvalid"));
|
|
1208
1605
|
const clientId = createClientId2();
|
|
1209
|
-
unwrapResult(
|
|
1606
|
+
const applied = unwrapResult(
|
|
1210
1607
|
"cache.kv.set",
|
|
1211
1608
|
await options.cache.kv.set(
|
|
1212
1609
|
buildCacheKey(clientId),
|
|
@@ -1214,6 +1611,8 @@ function createRedisTransportKeyStore(options) {
|
|
|
1214
1611
|
ttlSeconds === void 0 ? void 0 : { ex: ttlSeconds }
|
|
1215
1612
|
)
|
|
1216
1613
|
);
|
|
1614
|
+
if (!applied)
|
|
1615
|
+
throw new Error(cryptoM("crypto_transportClientNotRegistered"));
|
|
1217
1616
|
return clientId;
|
|
1218
1617
|
},
|
|
1219
1618
|
async get(clientId) {
|
|
@@ -1223,6 +1622,17 @@ function createRedisTransportKeyStore(options) {
|
|
|
1223
1622
|
);
|
|
1224
1623
|
return publicKey ?? void 0;
|
|
1225
1624
|
},
|
|
1625
|
+
async claimRequest(clientId, requestId, expiresAt) {
|
|
1626
|
+
const ttl = expiresAt - Date.now();
|
|
1627
|
+
if (ttl <= 0)
|
|
1628
|
+
return false;
|
|
1629
|
+
return unwrapResult("cache.kv.compareAndSwap", await options.cache.kv.compareAndSwap(
|
|
1630
|
+
`hai:crypto:transport:request:${clientId}:${requestId}`,
|
|
1631
|
+
void 0,
|
|
1632
|
+
true,
|
|
1633
|
+
{ px: ttl }
|
|
1634
|
+
));
|
|
1635
|
+
},
|
|
1226
1636
|
async delete(clientId) {
|
|
1227
1637
|
unwrapResult("cache.kv.del", await options.cache.kv.del(buildCacheKey(clientId)));
|
|
1228
1638
|
}
|
|
@@ -1231,20 +1641,11 @@ function createRedisTransportKeyStore(options) {
|
|
|
1231
1641
|
|
|
1232
1642
|
// src/transport/store-provider/crypto-transport-store-reldb.ts
|
|
1233
1643
|
var TRANSPORT_KEY_TABLE = "hai_crypto_transport_client_keys";
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
return Math.floor(ttlSeconds);
|
|
1238
|
-
}
|
|
1239
|
-
function createClientId3() {
|
|
1240
|
-
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function")
|
|
1241
|
-
return `c_${crypto.randomUUID()}`;
|
|
1242
|
-
return `c_${Date.now()}_${Math.random().toString(36).slice(2, 12)}`;
|
|
1243
|
-
}
|
|
1644
|
+
var TRANSPORT_REQUEST_TABLE = "hai_crypto_transport_requests";
|
|
1645
|
+
var TRANSPORT_CAPACITY_TABLE = "hai_crypto_transport_capacity";
|
|
1646
|
+
var PRUNE_BATCH_SIZE = 100;
|
|
1244
1647
|
function toOperationError2(operation, error) {
|
|
1245
|
-
|
|
1246
|
-
wrapped.code = error.code;
|
|
1247
|
-
return wrapped;
|
|
1648
|
+
return Object.assign(new Error(cryptoM("crypto_transportKeyStoreOperationFailed")), { operation, code: error.code });
|
|
1248
1649
|
}
|
|
1249
1650
|
function unwrapResult2(operation, result) {
|
|
1250
1651
|
if (result.success)
|
|
@@ -1252,21 +1653,66 @@ function unwrapResult2(operation, result) {
|
|
|
1252
1653
|
throw toOperationError2(operation, result.error);
|
|
1253
1654
|
}
|
|
1254
1655
|
function createReldbTransportKeyStore(options) {
|
|
1255
|
-
const ttlSeconds =
|
|
1656
|
+
const { ttlSeconds } = options;
|
|
1657
|
+
if (ttlSeconds !== void 0 && (!Number.isSafeInteger(ttlSeconds) || ttlSeconds <= 0 || ttlSeconds > 2147483647))
|
|
1658
|
+
throw new TypeError(cryptoM("crypto_transportKeyStoreTtlInvalid"));
|
|
1659
|
+
const maxClients = options.maxClients ?? 1e4;
|
|
1660
|
+
if (!Number.isSafeInteger(maxClients) || maxClients <= 0)
|
|
1661
|
+
throw new TypeError(cryptoM("crypto_transportKeyStoreCapacityInvalid"));
|
|
1256
1662
|
let ensureTablePromise = null;
|
|
1663
|
+
function nowSql() {
|
|
1664
|
+
switch (options.reldb.config?.type) {
|
|
1665
|
+
case "postgresql":
|
|
1666
|
+
return "CAST(FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000) AS BIGINT)";
|
|
1667
|
+
case "mysql":
|
|
1668
|
+
return "CAST(FLOOR(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000) AS SIGNED)";
|
|
1669
|
+
default:
|
|
1670
|
+
return "(CAST(strftime('%s', 'now') AS INTEGER) * 1000 + CAST(substr(strftime('%f', 'now'), 4, 3) AS INTEGER))";
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
function insertIfAbsent(table, columns, placeholders, key) {
|
|
1674
|
+
return options.reldb.config?.type === "mysql" ? `INSERT IGNORE INTO ${table} (${columns}) VALUES (${placeholders})` : `INSERT INTO ${table} (${columns}) VALUES (${placeholders}) ON CONFLICT (${key}) DO NOTHING`;
|
|
1675
|
+
}
|
|
1257
1676
|
async function ensureTable() {
|
|
1258
1677
|
if (!ensureTablePromise) {
|
|
1259
|
-
|
|
1678
|
+
const initialization = (async () => {
|
|
1260
1679
|
unwrapResult2(
|
|
1261
1680
|
"reldb.ddl.createTable",
|
|
1262
1681
|
await options.reldb.ddl.createTable(TRANSPORT_KEY_TABLE, {
|
|
1263
1682
|
client_id: { type: "TEXT", primaryKey: true },
|
|
1264
1683
|
public_key: { type: "TEXT", notNull: true },
|
|
1265
|
-
|
|
1266
|
-
|
|
1684
|
+
created_at_ms: { type: "BIGINT", notNull: true },
|
|
1685
|
+
expires_at_ms: { type: "BIGINT" }
|
|
1267
1686
|
}, true)
|
|
1268
1687
|
);
|
|
1688
|
+
unwrapResult2("reldb.ddl.createTable", await options.reldb.ddl.createTable(TRANSPORT_REQUEST_TABLE, {
|
|
1689
|
+
request_key: { type: "TEXT", primaryKey: true },
|
|
1690
|
+
expires_at: { type: "BIGINT", notNull: true }
|
|
1691
|
+
}, true));
|
|
1692
|
+
unwrapResult2("reldb.ddl.createTable", await options.reldb.ddl.createTable(TRANSPORT_CAPACITY_TABLE, {
|
|
1693
|
+
id: { type: "INTEGER", primaryKey: true },
|
|
1694
|
+
max_clients: { type: "BIGINT", notNull: true },
|
|
1695
|
+
revision: { type: "BIGINT", notNull: true }
|
|
1696
|
+
}, true));
|
|
1697
|
+
unwrapResult2("reldb.sql.execute", await options.reldb.sql.execute(
|
|
1698
|
+
insertIfAbsent(TRANSPORT_CAPACITY_TABLE, "id, max_clients, revision", "?, ?, ?", "id"),
|
|
1699
|
+
[1, maxClients, 0]
|
|
1700
|
+
));
|
|
1701
|
+
const capacity = unwrapResult2("reldb.sql.get", await options.reldb.sql.get(
|
|
1702
|
+
`SELECT max_clients FROM ${TRANSPORT_CAPACITY_TABLE} WHERE id = ?`,
|
|
1703
|
+
[1]
|
|
1704
|
+
));
|
|
1705
|
+
if (!capacity || Number(capacity.max_clients) !== maxClients)
|
|
1706
|
+
throw new Error(cryptoM("crypto_transportKeyStoreCapacityMismatch"));
|
|
1269
1707
|
})();
|
|
1708
|
+
ensureTablePromise = initialization;
|
|
1709
|
+
try {
|
|
1710
|
+
await initialization;
|
|
1711
|
+
} catch (error) {
|
|
1712
|
+
if (ensureTablePromise === initialization)
|
|
1713
|
+
ensureTablePromise = null;
|
|
1714
|
+
throw error;
|
|
1715
|
+
}
|
|
1270
1716
|
}
|
|
1271
1717
|
return ensureTablePromise;
|
|
1272
1718
|
}
|
|
@@ -1279,45 +1725,88 @@ function createReldbTransportKeyStore(options) {
|
|
|
1279
1725
|
)
|
|
1280
1726
|
);
|
|
1281
1727
|
}
|
|
1728
|
+
async function withCapacityLock(operation) {
|
|
1729
|
+
await ensureTable();
|
|
1730
|
+
const result = await options.reldb.tx.wrap(async (tx) => {
|
|
1731
|
+
const locked = unwrapResult2("tx.execute", await tx.execute(
|
|
1732
|
+
`UPDATE ${TRANSPORT_CAPACITY_TABLE} SET revision = revision + 1 WHERE id = ? AND max_clients = ?`,
|
|
1733
|
+
[1, maxClients]
|
|
1734
|
+
));
|
|
1735
|
+
if (locked.changes !== 1)
|
|
1736
|
+
throw new Error(cryptoM("crypto_transportKeyStoreCapacityMismatch"));
|
|
1737
|
+
return operation(tx);
|
|
1738
|
+
});
|
|
1739
|
+
return unwrapResult2("reldb.tx.wrap", result);
|
|
1740
|
+
}
|
|
1741
|
+
async function pruneRows(tx) {
|
|
1742
|
+
let removed = 0;
|
|
1743
|
+
for (const [table, id, expiry] of [[TRANSPORT_KEY_TABLE, "client_id", "expires_at_ms"], [TRANSPORT_REQUEST_TABLE, "request_key", "expires_at"]]) {
|
|
1744
|
+
removed += unwrapResult2("tx.execute", await tx.execute(
|
|
1745
|
+
`DELETE FROM ${table} WHERE ${expiry} <= ${nowSql()} AND ${id} IN (SELECT ${id} FROM (SELECT ${id} FROM ${table} WHERE ${expiry} <= ${nowSql()} LIMIT ${PRUNE_BATCH_SIZE}) AS expired)`
|
|
1746
|
+
)).changes;
|
|
1747
|
+
}
|
|
1748
|
+
return removed;
|
|
1749
|
+
}
|
|
1750
|
+
async function pruneExpired() {
|
|
1751
|
+
return withCapacityLock(pruneRows);
|
|
1752
|
+
}
|
|
1282
1753
|
return {
|
|
1754
|
+
get stateScope() {
|
|
1755
|
+
return options.reldb.config?.type && options.reldb.config.type !== "sqlite" ? "shared" : "local";
|
|
1756
|
+
},
|
|
1283
1757
|
async register(publicKey) {
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
"
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
)
|
|
1294
|
-
|
|
1295
|
-
|
|
1758
|
+
if (!isValidSM2PublicKey(publicKey))
|
|
1759
|
+
throw new TypeError(cryptoM("crypto_sm2PublicKeyInvalid"));
|
|
1760
|
+
const clientId = `c_${globalThis.crypto.randomUUID()}`;
|
|
1761
|
+
return withCapacityLock(async (tx) => {
|
|
1762
|
+
await pruneRows(tx);
|
|
1763
|
+
const count = unwrapResult2("tx.get", await tx.get(
|
|
1764
|
+
`SELECT COUNT(*) AS total FROM ${TRANSPORT_KEY_TABLE} WHERE expires_at_ms IS NULL OR expires_at_ms > ${nowSql()}`
|
|
1765
|
+
));
|
|
1766
|
+
const total = Number(count?.total);
|
|
1767
|
+
if (!Number.isSafeInteger(total) || total < 0)
|
|
1768
|
+
throw new Error(cryptoM("crypto_transportKeyStoreOperationFailed"));
|
|
1769
|
+
if (total >= maxClients)
|
|
1770
|
+
throw new Error(cryptoM("crypto_transportKeyStoreCapacityReached"));
|
|
1771
|
+
const inserted = unwrapResult2("tx.execute", await tx.execute(
|
|
1772
|
+
`INSERT INTO ${TRANSPORT_KEY_TABLE} (client_id, public_key, created_at_ms, expires_at_ms) VALUES (?, ?, ${nowSql()}, ${ttlSeconds === void 0 ? "NULL" : `${nowSql()} + ?`})`,
|
|
1773
|
+
ttlSeconds === void 0 ? [clientId, publicKey] : [clientId, publicKey, ttlSeconds * 1e3]
|
|
1774
|
+
));
|
|
1775
|
+
if (inserted.changes !== 1)
|
|
1776
|
+
throw new Error(cryptoM("crypto_transportClientNotRegistered"));
|
|
1777
|
+
return clientId;
|
|
1778
|
+
});
|
|
1296
1779
|
},
|
|
1297
1780
|
async get(clientId) {
|
|
1298
1781
|
await ensureTable();
|
|
1299
1782
|
const row = unwrapResult2(
|
|
1300
1783
|
"reldb.sql.get",
|
|
1301
1784
|
await options.reldb.sql.get(
|
|
1302
|
-
`SELECT public_key
|
|
1785
|
+
`SELECT public_key FROM ${TRANSPORT_KEY_TABLE} WHERE client_id = ? AND (expires_at_ms IS NULL OR expires_at_ms > ${nowSql()})`,
|
|
1303
1786
|
[clientId]
|
|
1304
1787
|
)
|
|
1305
1788
|
);
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1789
|
+
return row?.public_key;
|
|
1790
|
+
},
|
|
1791
|
+
async claimRequest(clientId, requestId, expiresAt) {
|
|
1792
|
+
await ensureTable();
|
|
1793
|
+
if (!Number.isSafeInteger(expiresAt) || expiresAt <= Date.now())
|
|
1794
|
+
return false;
|
|
1795
|
+
const insertSql = options.reldb.config?.type === "mysql" ? `INSERT IGNORE INTO ${TRANSPORT_REQUEST_TABLE} (request_key, expires_at) VALUES (?, ?)` : `INSERT INTO ${TRANSPORT_REQUEST_TABLE} (request_key, expires_at) VALUES (?, ?) ON CONFLICT (request_key) DO NOTHING`;
|
|
1796
|
+
const inserted = unwrapResult2("reldb.sql.execute", await options.reldb.sql.execute(
|
|
1797
|
+
insertSql,
|
|
1798
|
+
[JSON.stringify([clientId, requestId]), expiresAt]
|
|
1799
|
+
));
|
|
1800
|
+
return inserted.changes === 1;
|
|
1313
1801
|
},
|
|
1314
1802
|
async delete(clientId) {
|
|
1315
1803
|
await ensureTable();
|
|
1316
1804
|
await deleteClientKey(clientId);
|
|
1317
|
-
}
|
|
1805
|
+
},
|
|
1806
|
+
pruneExpired
|
|
1318
1807
|
};
|
|
1319
1808
|
}
|
|
1320
1809
|
|
|
1321
|
-
export { HaiCryptoError, TRANSPORT_PROTOCOL, createInMemoryKeyStore, createRedisTransportKeyStore, createReldbTransportKeyStore, crypto2 as crypto };
|
|
1810
|
+
export { HaiCryptoError, TRANSPORT_BODY_LIMITS, TRANSPORT_PROTOCOL, authenticateTransportRequest, createInMemoryKeyStore, createRedisTransportKeyStore, createReldbTransportKeyStore, crypto2 as crypto, isValidSM2PublicKey, parseTransportServerKeyRing, readTransportBody };
|
|
1322
1811
|
//# sourceMappingURL=index.js.map
|
|
1323
1812
|
//# sourceMappingURL=index.js.map
|