@h-ai/crypto 0.1.0-alpha.67 → 0.1.0-alpha.69
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 +67 -16
- package/README.ja.md +67 -16
- package/README.md +67 -16
- package/dist/index.d.ts +1720 -177
- package/dist/index.js +839 -235
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
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: "The server transport key ring is missing or invalid. Check that private HAI_CRYPTO_TRANSPORT_SERVERKEYRING contains complete valid JSON and distribute the same ring to every node.",
|
|
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",
|
|
@@ -47,7 +55,68 @@ var en_US_default = {
|
|
|
47
55
|
crypto_transportResponseEncryptFailed: "Failed to encrypt transport response body",
|
|
48
56
|
crypto_transportResponseDecryptFailed: "Failed to decrypt transport response body",
|
|
49
57
|
crypto_transportSessionKeyEncryptFailed: "Failed to encrypt transport session key",
|
|
50
|
-
crypto_transportSessionKeyDecryptFailed: "Failed to decrypt transport session key"
|
|
58
|
+
crypto_transportSessionKeyDecryptFailed: "Failed to decrypt transport session key",
|
|
59
|
+
crypto_transportBodyLimitInvalid: "Transport plaintext limit must be an integer between 1024 and 67108864 bytes",
|
|
60
|
+
crypto_transportBodyTooLarge: "Encrypted transport payload exceeds the configured byte limit",
|
|
61
|
+
crypto_transportTimeInvalid: "Your device and server clocks may be out of sync, or the request has expired. Enable automatic time synchronization and refresh to retry. If this continues, ask an administrator to check the server clock."
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// messages/ja-JP.json
|
|
65
|
+
var ja_JP_default = {
|
|
66
|
+
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
67
|
+
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",
|
|
68
|
+
crypto_initFailed: "\u6697\u53F7\u30E2\u30B8\u30E5\u30FC\u30EB\u306E\u521D\u671F\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
69
|
+
crypto_sm2PublicKeyInvalid: "SM2\u516C\u958B\u9375\u306E\u5F62\u5F0F\u304C\u7121\u52B9\u3067\u3059",
|
|
70
|
+
crypto_sm2PrivateKeyInvalid: "SM2\u79D8\u5BC6\u9375\u306E\u5F62\u5F0F\u304C\u7121\u52B9\u3067\u3059",
|
|
71
|
+
crypto_sm2EncryptEmpty: "SM2\u6697\u53F7\u5316\u306E\u7D50\u679C\u304C\u7A7A\u3067\u3059",
|
|
72
|
+
crypto_sm2DecryptFailed: "SM2\u5FA9\u53F7\u306B\u5931\u6557\u3057\u305F\u304B\u3001\u7D50\u679C\u304C\u7121\u52B9\u3067\u3059",
|
|
73
|
+
crypto_sm2SignEmpty: "SM2\u7F72\u540D\u306E\u7D50\u679C\u304C\u7A7A\u3067\u3059",
|
|
74
|
+
crypto_sm2KeyPairGenerateFailed: "SM2\u9375\u30DA\u30A2\u306E\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
75
|
+
crypto_sm2EncryptFailed: "SM2\u6697\u53F7\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
76
|
+
crypto_sm2DecryptFailedWithError: "SM2\u5FA9\u53F7\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
77
|
+
crypto_sm2SignFailed: "SM2\u7F72\u540D\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
78
|
+
crypto_sm2VerifyFailed: "SM2\u7F72\u540D\u306E\u691C\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
79
|
+
crypto_sm3HashEmpty: "SM3\u30CF\u30C3\u30B7\u30E5\u306E\u7D50\u679C\u304C\u7A7A\u3067\u3059",
|
|
80
|
+
crypto_sm3HashFailed: "SM3\u30CF\u30C3\u30B7\u30E5\u306E\u8A08\u7B97\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
81
|
+
crypto_sm3HmacFailed: "HMAC-SM3\u306E\u8A08\u7B97\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
82
|
+
crypto_sm3VerifyFailed: "SM3\u30CF\u30C3\u30B7\u30E5\u306E\u691C\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
83
|
+
crypto_sm4KeyInvalid: "SM4\u9375\u306F16\u30D0\u30A4\u30C8\uFF0816\u9032\u657032\u6587\u5B57\uFF09\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059",
|
|
84
|
+
crypto_sm4CbcNeedIv: "CBC\u30E2\u30FC\u30C9\u306B\u306FIV\u304C\u5FC5\u8981\u3067\u3059",
|
|
85
|
+
crypto_sm4IvInvalid: "SM4\u306EIV\u306F16\u30D0\u30A4\u30C8\uFF0816\u9032\u657032\u6587\u5B57\uFF09\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059",
|
|
86
|
+
crypto_sm4EncryptEmpty: "SM4\u6697\u53F7\u5316\u306E\u7D50\u679C\u304C\u7A7A\u3067\u3059",
|
|
87
|
+
crypto_sm4DecryptFailed: "SM4\u5FA9\u53F7\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
88
|
+
crypto_sm4EncryptFailed: "SM4\u6697\u53F7\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
89
|
+
crypto_sm4DecryptFailedWithError: "SM4\u5FA9\u53F7\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
90
|
+
crypto_passwordEmpty: "\u30D1\u30B9\u30EF\u30FC\u30C9\u3092\u7A7A\u306B\u3059\u308B\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093",
|
|
91
|
+
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",
|
|
92
|
+
crypto_passwordHashFailed: "\u30D1\u30B9\u30EF\u30FC\u30C9\u306E\u30CF\u30C3\u30B7\u30E5\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
93
|
+
crypto_hashFormatInvalid: "\u30CF\u30C3\u30B7\u30E5\u306E\u5F62\u5F0F\u304C\u7121\u52B9\u3067\u3059",
|
|
94
|
+
crypto_passwordVerifyFailed: "\u30D1\u30B9\u30EF\u30FC\u30C9\u306E\u691C\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
95
|
+
crypto_transportClientKeyGenerateFailed: "\u901A\u4FE1\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u306E\u9375\u30DA\u30A2\u306E\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
96
|
+
crypto_transportServerKeyRingInvalid: "\u30B5\u30FC\u30D0\u30FC\u306E\u8EE2\u9001\u9375\u30EA\u30F3\u30B0\u304C\u4E0D\u8DB3\u307E\u305F\u306F\u4E0D\u6B63\u3067\u3059\u3002\u975E\u516C\u958B\u306E HAI_CRYPTO_TRANSPORT_SERVERKEYRING \u304C\u5B8C\u5168\u3067\u6709\u52B9\u306A JSON \u3067\u3042\u308B\u3053\u3068\u3092\u78BA\u8A8D\u3057\u3001\u5168\u30CE\u30FC\u30C9\u306B\u540C\u3058\u30EA\u30F3\u30B0\u3092\u914D\u5E03\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
97
|
+
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",
|
|
98
|
+
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",
|
|
99
|
+
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",
|
|
100
|
+
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",
|
|
101
|
+
crypto_transportKeyStoreCapacityMismatch: "\u516C\u958B\u30AD\u30FC\u30B9\u30C8\u30A2\u306E\u5171\u6709\u5BB9\u91CF\u8A2D\u5B9A\u304C\u4E00\u81F4\u3057\u307E\u305B\u3093",
|
|
102
|
+
crypto_transportKeyStoreCapacityReached: "\u516C\u958B\u30AD\u30FC\u30B9\u30C8\u30A2\u306E\u5BB9\u91CF\u4E0A\u9650\u306B\u9054\u3057\u307E\u3057\u305F",
|
|
103
|
+
crypto_transportKeyStoreOperationFailed: "\u516C\u958B\u30AD\u30FC\u30B9\u30C8\u30A2\u306E\u64CD\u4F5C\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
104
|
+
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",
|
|
105
|
+
crypto_transportKeyExchangeRequestFailed: "\u901A\u4FE1\u306E\u9375\u4EA4\u63DB\u30EA\u30AF\u30A8\u30B9\u30C8\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
106
|
+
crypto_transportKeyExchangeHttpFailed: "\u901A\u4FE1\u306E\u9375\u4EA4\u63DB\u3067HTTP {status}\u304C\u8FD4\u3055\u308C\u307E\u3057\u305F",
|
|
107
|
+
crypto_transportKeyExchangeInvalidJson: "\u901A\u4FE1\u306E\u9375\u4EA4\u63DB\u30EC\u30B9\u30DD\u30F3\u30B9\u304C\u6709\u52B9\u306AJSON\u3067\u306F\u3042\u308A\u307E\u305B\u3093",
|
|
108
|
+
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",
|
|
109
|
+
crypto_transportClientNotInitialized: "\u901A\u4FE1\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u306E\u9375\u4EA4\u63DB\u304C\u5B8C\u4E86\u3057\u3066\u3044\u307E\u305B\u3093",
|
|
110
|
+
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",
|
|
111
|
+
crypto_transportRequestEncryptFailed: "\u901A\u4FE1\u30EA\u30AF\u30A8\u30B9\u30C8\u672C\u6587\u306E\u6697\u53F7\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
112
|
+
crypto_transportRequestDecryptFailed: "\u901A\u4FE1\u30EA\u30AF\u30A8\u30B9\u30C8\u672C\u6587\u306E\u5FA9\u53F7\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
113
|
+
crypto_transportResponseEncryptFailed: "\u901A\u4FE1\u30EC\u30B9\u30DD\u30F3\u30B9\u672C\u6587\u306E\u6697\u53F7\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
114
|
+
crypto_transportResponseDecryptFailed: "\u901A\u4FE1\u30EC\u30B9\u30DD\u30F3\u30B9\u672C\u6587\u306E\u5FA9\u53F7\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
115
|
+
crypto_transportSessionKeyEncryptFailed: "\u901A\u4FE1\u30BB\u30C3\u30B7\u30E7\u30F3\u9375\u306E\u6697\u53F7\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
116
|
+
crypto_transportSessionKeyDecryptFailed: "\u901A\u4FE1\u30BB\u30C3\u30B7\u30E7\u30F3\u9375\u306E\u5FA9\u53F7\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
117
|
+
crypto_transportBodyLimitInvalid: "\u901A\u4FE1\u5E73\u6587\u306E\u4E0A\u9650\u306F 1024 \u304B\u3089 67108864 \u30D0\u30A4\u30C8\u306E\u6574\u6570\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
118
|
+
crypto_transportBodyTooLarge: "\u6697\u53F7\u5316\u901A\u4FE1\u306E\u30C7\u30FC\u30BF\u304C\u8A2D\u5B9A\u3055\u308C\u305F\u30D0\u30A4\u30C8\u4E0A\u9650\u3092\u8D85\u3048\u3066\u3044\u307E\u3059",
|
|
119
|
+
crypto_transportTimeInvalid: "\u7AEF\u672B\u3068\u30B5\u30FC\u30D0\u30FC\u306E\u6642\u523B\u304C\u540C\u671F\u3057\u3066\u3044\u306A\u3044\u304B\u3001\u8981\u6C42\u306E\u6709\u52B9\u671F\u9650\u304C\u5207\u308C\u3066\u3044\u307E\u3059\u3002\u81EA\u52D5\u6642\u523B\u540C\u671F\u3092\u6709\u52B9\u306B\u3057\u3066\u30DA\u30FC\u30B8\u3092\u518D\u8AAD\u307F\u8FBC\u307F\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u89E3\u6D88\u3057\u306A\u3044\u5834\u5408\u306F\u7BA1\u7406\u8005\u306B\u30B5\u30FC\u30D0\u30FC\u306E\u6642\u523B\u78BA\u8A8D\u3092\u4F9D\u983C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
51
120
|
};
|
|
52
121
|
|
|
53
122
|
// messages/zh-CN.json
|
|
@@ -82,7 +151,15 @@ var zh_CN_default = {
|
|
|
82
151
|
crypto_hashFormatInvalid: "\u65E0\u6548\u7684\u54C8\u5E0C\u683C\u5F0F",
|
|
83
152
|
crypto_passwordVerifyFailed: "\u5BC6\u7801\u9A8C\u8BC1\u5931\u8D25",
|
|
84
153
|
crypto_transportClientKeyGenerateFailed: "\u4F20\u8F93\u52A0\u5BC6\u5BA2\u6237\u7AEF\u5BC6\u94A5\u751F\u6210\u5931\u8D25",
|
|
85
|
-
|
|
154
|
+
crypto_transportServerKeyRingInvalid: "\u670D\u52A1\u7AEF\u4F20\u8F93\u5BC6\u94A5\u73AF\u7F3A\u5931\u6216\u65E0\u6548\u3002\u8BF7\u68C0\u67E5\u79C1\u6709\u73AF\u5883\u53D8\u91CF HAI_CRYPTO_TRANSPORT_SERVERKEYRING \u662F\u5426\u4E3A\u5B8C\u6574\u6709\u6548\u7684 JSON\uFF0C\u5E76\u5411\u6240\u6709\u8282\u70B9\u5206\u53D1\u540C\u4E00\u5BC6\u94A5\u73AF\u3002",
|
|
155
|
+
crypto_transportServerKeyUnknown: "\u670D\u52A1\u7AEF\u4F20\u8F93\u5BC6\u94A5\u7248\u672C\u672A\u77E5\u6216\u5DF2\u64A4\u9500",
|
|
156
|
+
crypto_transportKeyStoreRequired: "\u4F20\u8F93\u52A0\u5BC6\u5FC5\u987B\u663E\u5F0F\u63D0\u4F9B\u5BA2\u6237\u7AEF\u516C\u94A5\u5B58\u50A8",
|
|
157
|
+
crypto_transportKeyStoreTtlInvalid: "\u516C\u94A5\u5B58\u50A8 TTL \u5FC5\u987B\u4E3A 1 \u5230 2147483647 \u4E4B\u95F4\u7684\u6574\u6570\u79D2",
|
|
158
|
+
crypto_transportKeyStoreCapacityInvalid: "\u516C\u94A5\u5B58\u50A8\u5BB9\u91CF\u5FC5\u987B\u4E3A\u6B63\u5B89\u5168\u6574\u6570",
|
|
159
|
+
crypto_transportKeyStoreCapacityMismatch: "\u516C\u94A5\u5B58\u50A8\u7684\u5171\u4EAB\u5BB9\u91CF\u914D\u7F6E\u4E0D\u4E00\u81F4",
|
|
160
|
+
crypto_transportKeyStoreCapacityReached: "\u516C\u94A5\u5B58\u50A8\u5BB9\u91CF\u5DF2\u6EE1",
|
|
161
|
+
crypto_transportKeyStoreOperationFailed: "\u516C\u94A5\u5B58\u50A8\u64CD\u4F5C\u5931\u8D25",
|
|
162
|
+
crypto_transportAuthenticationFailed: "\u4F20\u8F93\u6D88\u606F\u8BA4\u8BC1\u3001\u8BF7\u6C42\u7ED1\u5B9A\u6216\u9632\u91CD\u653E\u6821\u9A8C\u5931\u8D25",
|
|
86
163
|
crypto_transportKeyExchangeRequestFailed: "\u4F20\u8F93\u52A0\u5BC6\u5BC6\u94A5\u534F\u5546\u8BF7\u6C42\u5931\u8D25",
|
|
87
164
|
crypto_transportKeyExchangeHttpFailed: "\u4F20\u8F93\u52A0\u5BC6\u5BC6\u94A5\u534F\u5546\u8FD4\u56DE HTTP {status}",
|
|
88
165
|
crypto_transportKeyExchangeInvalidJson: "\u4F20\u8F93\u52A0\u5BC6\u5BC6\u94A5\u534F\u5546\u54CD\u5E94\u4E0D\u662F\u6709\u6548 JSON",
|
|
@@ -94,13 +171,17 @@ var zh_CN_default = {
|
|
|
94
171
|
crypto_transportResponseEncryptFailed: "\u4F20\u8F93\u52A0\u5BC6\u54CD\u5E94\u4F53\u52A0\u5BC6\u5931\u8D25",
|
|
95
172
|
crypto_transportResponseDecryptFailed: "\u4F20\u8F93\u52A0\u5BC6\u54CD\u5E94\u4F53\u89E3\u5BC6\u5931\u8D25",
|
|
96
173
|
crypto_transportSessionKeyEncryptFailed: "\u4F20\u8F93\u52A0\u5BC6\u4F1A\u8BDD\u5BC6\u94A5\u52A0\u5BC6\u5931\u8D25",
|
|
97
|
-
crypto_transportSessionKeyDecryptFailed: "\u4F20\u8F93\u52A0\u5BC6\u4F1A\u8BDD\u5BC6\u94A5\u89E3\u5BC6\u5931\u8D25"
|
|
174
|
+
crypto_transportSessionKeyDecryptFailed: "\u4F20\u8F93\u52A0\u5BC6\u4F1A\u8BDD\u5BC6\u94A5\u89E3\u5BC6\u5931\u8D25",
|
|
175
|
+
crypto_transportBodyLimitInvalid: "\u4F20\u8F93\u660E\u6587\u5BB9\u91CF\u5FC5\u987B\u662F 1024 \u5230 67108864 \u5B57\u8282\u4E4B\u95F4\u7684\u6574\u6570",
|
|
176
|
+
crypto_transportBodyTooLarge: "\u52A0\u5BC6\u4F20\u8F93\u62A5\u6587\u8D85\u8FC7\u914D\u7F6E\u7684\u5B57\u8282\u4E0A\u9650",
|
|
177
|
+
crypto_transportTimeInvalid: "\u8BBE\u5907\u4E0E\u670D\u52A1\u5668\u65F6\u95F4\u53EF\u80FD\u4E0D\u540C\u6B65\uFF0C\u6216\u8BF7\u6C42\u5DF2\u8FC7\u671F\u3002\u8BF7\u5F00\u542F\u81EA\u52A8\u6821\u65F6\u5E76\u5237\u65B0\u91CD\u8BD5\uFF1B\u82E5\u4ECD\u5931\u8D25\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u68C0\u67E5\u670D\u52A1\u5668\u65F6\u95F4\u3002"
|
|
98
178
|
};
|
|
99
179
|
|
|
100
180
|
// src/crypto-i18n.ts
|
|
101
181
|
var cryptoM = core.i18n.createMessageGetter({
|
|
102
182
|
"zh-CN": zh_CN_default,
|
|
103
|
-
"en-US": en_US_default
|
|
183
|
+
"en-US": en_US_default,
|
|
184
|
+
"ja-JP": ja_JP_default
|
|
104
185
|
});
|
|
105
186
|
var CryptoErrorInfo = {
|
|
106
187
|
INVALID_INPUT: "002:400",
|
|
@@ -114,7 +195,9 @@ var CryptoErrorInfo = {
|
|
|
114
195
|
VERIFY_FAILED: "024:500",
|
|
115
196
|
HASH_FAILED: "040:500",
|
|
116
197
|
HMAC_FAILED: "041:500",
|
|
117
|
-
INVALID_IV: "060:400"
|
|
198
|
+
INVALID_IV: "060:400",
|
|
199
|
+
TRANSPORT_PAYLOAD_TOO_LARGE: "070:413",
|
|
200
|
+
TRANSPORT_TIME_INVALID: "071:400"
|
|
118
201
|
};
|
|
119
202
|
var HaiCryptoError = core.error.buildHaiErrorsDef("crypto", CryptoErrorInfo);
|
|
120
203
|
|
|
@@ -241,6 +324,13 @@ function createPasswordFunctions(deps) {
|
|
|
241
324
|
}
|
|
242
325
|
};
|
|
243
326
|
}
|
|
327
|
+
var SM2_PUBLIC_KEY_REGEX = /^(?:04)?[0-9a-f]{128}$/i;
|
|
328
|
+
function isValidSM2PublicKey(publicKey) {
|
|
329
|
+
if (typeof publicKey !== "string" || publicKey.length !== 128 && publicKey.length !== 130 || !SM2_PUBLIC_KEY_REGEX.test(publicKey))
|
|
330
|
+
return false;
|
|
331
|
+
const encoded = publicKey.length === 128 ? `04${publicKey}` : publicKey;
|
|
332
|
+
return smCrypto.sm2.verifyPublicKey(encoded) === true;
|
|
333
|
+
}
|
|
244
334
|
|
|
245
335
|
// src/crypto-utils.ts
|
|
246
336
|
function isBase64(str) {
|
|
@@ -264,7 +354,6 @@ function base64ToHex(base64) {
|
|
|
264
354
|
|
|
265
355
|
// src/crypto-sm2.ts
|
|
266
356
|
var { sm2 } = smCrypto;
|
|
267
|
-
var SM2_PUBLIC_KEY_REGEX = /^[0-9a-f]{128}$/i;
|
|
268
357
|
var SM2_PRIVATE_KEY_REGEX = /^[0-9a-f]{64}$/i;
|
|
269
358
|
function createSM2() {
|
|
270
359
|
return {
|
|
@@ -307,7 +396,7 @@ function createSM2() {
|
|
|
307
396
|
);
|
|
308
397
|
}
|
|
309
398
|
try {
|
|
310
|
-
const key = publicKey.
|
|
399
|
+
const key = publicKey.length === 130 ? publicKey : `04${publicKey}`;
|
|
311
400
|
const encrypted = sm2.doEncrypt(data, key, cipherMode);
|
|
312
401
|
if (!encrypted) {
|
|
313
402
|
return err(
|
|
@@ -421,7 +510,7 @@ function createSM2() {
|
|
|
421
510
|
);
|
|
422
511
|
}
|
|
423
512
|
try {
|
|
424
|
-
const key = publicKey.
|
|
513
|
+
const key = publicKey.length === 130 ? publicKey : `04${publicKey}`;
|
|
425
514
|
const isValid = sm2.doVerifySignature(data, signature, key, { hash, userId });
|
|
426
515
|
return ok(!!isValid);
|
|
427
516
|
} catch (error) {
|
|
@@ -433,18 +522,16 @@ function createSM2() {
|
|
|
433
522
|
}
|
|
434
523
|
},
|
|
435
524
|
/**
|
|
436
|
-
*
|
|
525
|
+
* 校验公钥编码及曲线点是否合法
|
|
437
526
|
*
|
|
438
527
|
* 合法格式:128 字符十六进制(无前缀)或 130 字符(含 04 前缀)。
|
|
439
528
|
*
|
|
440
529
|
* @param key - 待校验公钥
|
|
441
|
-
* @returns
|
|
530
|
+
* @returns 编码有效且为 SM2 曲线上的非无穷远点时返回 true
|
|
531
|
+
* @example `crypto.asymmetric.isValidPublicKey(clientPublicKey)`
|
|
442
532
|
*/
|
|
443
533
|
isValidPublicKey(key) {
|
|
444
|
-
|
|
445
|
-
return false;
|
|
446
|
-
const cleanKey = key.startsWith("04") ? key.slice(2) : key;
|
|
447
|
-
return SM2_PUBLIC_KEY_REGEX.test(cleanKey);
|
|
534
|
+
return isValidSM2PublicKey(key);
|
|
448
535
|
},
|
|
449
536
|
/**
|
|
450
537
|
* 校验私钥格式是否合法
|
|
@@ -573,7 +660,7 @@ function stringToBytes(str) {
|
|
|
573
660
|
const encoder = new TextEncoder();
|
|
574
661
|
return Array.from(encoder.encode(str));
|
|
575
662
|
}
|
|
576
|
-
var {
|
|
663
|
+
var { sm4 } = smCrypto;
|
|
577
664
|
var SM4_HEX_32_REGEX = /^[0-9a-f]{32}$/i;
|
|
578
665
|
function createSM4() {
|
|
579
666
|
return {
|
|
@@ -591,7 +678,7 @@ function createSM4() {
|
|
|
591
678
|
* 默认使用 CBC 并自动生成随机 IV,返回结构化字段。
|
|
592
679
|
*
|
|
593
680
|
* ⚠️ 安全警告:ECB 模式会让相同明文块产生相同密文块,泄漏结构信息。
|
|
594
|
-
*
|
|
681
|
+
* 新数据使用默认 CBC,或每次显式传入新的随机 IV;CBC 仍需独立完整性认证,HTTP 使用 transport。
|
|
595
682
|
*
|
|
596
683
|
* @param data - 待加密明文
|
|
597
684
|
* @param key - 密钥(32 字符十六进制)
|
|
@@ -701,7 +788,7 @@ function createSM4() {
|
|
|
701
788
|
*
|
|
702
789
|
* @param data - 待加密明文
|
|
703
790
|
* @param key - 密钥(32 字符十六进制)
|
|
704
|
-
* @returns
|
|
791
|
+
* @returns 成功时返回完整 { mode, ciphertext, iv, encoding };失败时同 encrypt
|
|
705
792
|
*/
|
|
706
793
|
encryptWithIV(data, key) {
|
|
707
794
|
const iv = this.generateIV();
|
|
@@ -719,6 +806,8 @@ function createSM4() {
|
|
|
719
806
|
/**
|
|
720
807
|
* 带 IV 解密(CBC 模式)
|
|
721
808
|
*
|
|
809
|
+
* 这是不含认证的底层 primitive;传输数据必须经 transport 验签及 MAC 后再调用,禁止直接用于 HTTP 解密。
|
|
810
|
+
*
|
|
722
811
|
* @param ciphertext - 密文
|
|
723
812
|
* @param key - 密钥
|
|
724
813
|
* @param iv - 加密时使用的 IV
|
|
@@ -727,26 +816,6 @@ function createSM4() {
|
|
|
727
816
|
decryptWithIV(ciphertext, key, iv) {
|
|
728
817
|
return this.decrypt({ mode: "cbc", ciphertext, iv, encoding: "hex" }, key);
|
|
729
818
|
},
|
|
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
819
|
/** 校验密钥格式是否合法(32 字符十六进制) */
|
|
751
820
|
isValidKey(key) {
|
|
752
821
|
return SM4_HEX_32_REGEX.test(key);
|
|
@@ -769,10 +838,102 @@ function decodeCiphertext(payload) {
|
|
|
769
838
|
return payload.encoding === "base64" ? base64ToHex(payload.ciphertext) : payload.ciphertext;
|
|
770
839
|
}
|
|
771
840
|
|
|
841
|
+
// src/transport/crypto-transport-body.ts
|
|
842
|
+
var TRANSPORT_PLAINTEXT_RANGE = Object.freeze({ min: 1024, max: 64 * 1024 * 1024 });
|
|
843
|
+
var TRANSPORT_BODY_LIMITS = Object.freeze({
|
|
844
|
+
/** 密钥协商 JSON 的 UTF-8 总字节数。 */
|
|
845
|
+
keyExchangeBytes: 16384,
|
|
846
|
+
/** GET/HEAD 请求及无正文响应的认证头封装 UTF-8 上限。 */
|
|
847
|
+
envelopeHeaderBytes: 16384,
|
|
848
|
+
/** 单个协商公钥的 UTF-8 字节数。 */
|
|
849
|
+
publicKeyBytes: 4096,
|
|
850
|
+
/** 解密后的请求/加密前的响应明文字节数。 */
|
|
851
|
+
plaintextBytes: 1048576,
|
|
852
|
+
/** 密文 JSON 的 UTF-8 总字节数(2 MiB + 32 KiB,含 CBC 填充与全部认证字段)。 */
|
|
853
|
+
encryptedPayloadBytes: 2129920
|
|
854
|
+
});
|
|
855
|
+
function resolveTransportBodyLimits(maxPlaintextBytes = TRANSPORT_BODY_LIMITS.plaintextBytes) {
|
|
856
|
+
if (!Number.isSafeInteger(maxPlaintextBytes) || maxPlaintextBytes < TRANSPORT_PLAINTEXT_RANGE.min || maxPlaintextBytes > TRANSPORT_PLAINTEXT_RANGE.max)
|
|
857
|
+
throw new RangeError(cryptoM("crypto_transportBodyLimitInvalid"));
|
|
858
|
+
return Object.freeze({ ...TRANSPORT_BODY_LIMITS, plaintextBytes: maxPlaintextBytes, encryptedPayloadBytes: 2 * maxPlaintextBytes + 32768 });
|
|
859
|
+
}
|
|
860
|
+
var TransportBodyTooLargeError = class extends RangeError {
|
|
861
|
+
code = HaiCryptoError.TRANSPORT_PAYLOAD_TOO_LARGE.code;
|
|
862
|
+
direction;
|
|
863
|
+
stage;
|
|
864
|
+
limitBytes;
|
|
865
|
+
actualBytes;
|
|
866
|
+
constructor(limitBytes, actualBytes, options = {}) {
|
|
867
|
+
super(cryptoM("crypto_transportBodyTooLarge"));
|
|
868
|
+
this.name = "TransportBodyTooLargeError";
|
|
869
|
+
this.limitBytes = limitBytes;
|
|
870
|
+
this.actualBytes = actualBytes;
|
|
871
|
+
this.direction = options.direction ?? "request";
|
|
872
|
+
this.stage = options.stage ?? "plaintext";
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
async function readTransportBody(stream, options) {
|
|
876
|
+
if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0)
|
|
877
|
+
throw new RangeError("Invalid transport body byte limit");
|
|
878
|
+
if (!stream) {
|
|
879
|
+
options.signal?.throwIfAborted();
|
|
880
|
+
return "";
|
|
881
|
+
}
|
|
882
|
+
const reader = stream.getReader();
|
|
883
|
+
const signal = options.signal;
|
|
884
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
885
|
+
let completed = false;
|
|
886
|
+
let failure;
|
|
887
|
+
let onAbort;
|
|
888
|
+
try {
|
|
889
|
+
signal?.throwIfAborted();
|
|
890
|
+
const aborted = signal ? new Promise((_resolve, reject) => {
|
|
891
|
+
onAbort = () => reject(signal.reason);
|
|
892
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
893
|
+
}) : void 0;
|
|
894
|
+
let bytes = 0;
|
|
895
|
+
let text = "";
|
|
896
|
+
while (true) {
|
|
897
|
+
const next = reader.read();
|
|
898
|
+
const chunk = await (aborted ? Promise.race([next, aborted]) : next);
|
|
899
|
+
if (chunk.done) {
|
|
900
|
+
completed = true;
|
|
901
|
+
return text + decoder.decode();
|
|
902
|
+
}
|
|
903
|
+
bytes += chunk.value.byteLength;
|
|
904
|
+
if (bytes > options.maxBytes)
|
|
905
|
+
throw new TransportBodyTooLargeError(options.maxBytes, bytes, options);
|
|
906
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
907
|
+
}
|
|
908
|
+
} catch (error) {
|
|
909
|
+
failure = error;
|
|
910
|
+
throw error;
|
|
911
|
+
} finally {
|
|
912
|
+
if (onAbort)
|
|
913
|
+
signal?.removeEventListener("abort", onAbort);
|
|
914
|
+
if (!completed) {
|
|
915
|
+
try {
|
|
916
|
+
void reader.cancel(failure).catch(() => {
|
|
917
|
+
});
|
|
918
|
+
} catch {
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
reader.releaseLock();
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
772
925
|
// src/transport/crypto-transport-types.ts
|
|
773
926
|
var TRANSPORT_PROTOCOL = {
|
|
774
927
|
/** 客户端 ID 请求头名。 */
|
|
775
928
|
CLIENT_ID_HEADER: "X-Client-Id",
|
|
929
|
+
/** GET/HEAD 等无正文请求的认证封装;禁止与正文封装同时出现。 */
|
|
930
|
+
ENVELOPE_HEADER: "X-Hai-Transport",
|
|
931
|
+
/** 唯一支持的认证协议版本;旧未认证格式拒绝。 */
|
|
932
|
+
VERSION: 2,
|
|
933
|
+
/** 请求允许的过去时间窗口(毫秒)。 */
|
|
934
|
+
REQUEST_WINDOW_MS: 3e5,
|
|
935
|
+
/** 节点时钟允许的最大未来偏差(毫秒)。 */
|
|
936
|
+
CLOCK_SKEW_MS: 3e4,
|
|
776
937
|
/** 标识响应体已加密的响应头名。 */
|
|
777
938
|
ENCRYPTED_HEADER: "X-Encrypted",
|
|
778
939
|
/** `X-Encrypted` 响应头的开启值。 */
|
|
@@ -781,11 +942,138 @@ var TRANSPORT_PROTOCOL = {
|
|
|
781
942
|
DEFAULT_KEY_EXCHANGE_PATH: "/_hai/key-exchange"
|
|
782
943
|
};
|
|
783
944
|
|
|
945
|
+
// src/transport/crypto-transport-codec.ts
|
|
946
|
+
function authenticatedText(payload) {
|
|
947
|
+
const c = payload.context;
|
|
948
|
+
return JSON.stringify([
|
|
949
|
+
"hai-transport",
|
|
950
|
+
c.version,
|
|
951
|
+
payload.direction,
|
|
952
|
+
c.serverKeyId,
|
|
953
|
+
c.clientId,
|
|
954
|
+
c.requestId,
|
|
955
|
+
c.issuedAt,
|
|
956
|
+
c.method,
|
|
957
|
+
c.pathHash,
|
|
958
|
+
payload.status,
|
|
959
|
+
payload.encryptedKey,
|
|
960
|
+
payload.ciphertext,
|
|
961
|
+
payload.iv
|
|
962
|
+
]);
|
|
963
|
+
}
|
|
964
|
+
function transportAuthenticationFailure() {
|
|
965
|
+
return err(HaiCryptoError.INVALID_INPUT, cryptoM("crypto_transportAuthenticationFailed"));
|
|
966
|
+
}
|
|
967
|
+
function hashTransportPath(service, path) {
|
|
968
|
+
try {
|
|
969
|
+
if (!path.startsWith("/") || new TextEncoder().encode(path).byteLength > 8192)
|
|
970
|
+
return transportAuthenticationFailure();
|
|
971
|
+
const digest = service.hash.hash(path);
|
|
972
|
+
return digest.success && /^[a-f0-9]{64}$/.test(digest.data) ? digest : transportAuthenticationFailure();
|
|
973
|
+
} catch {
|
|
974
|
+
return transportAuthenticationFailure();
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
function sealTransportPayload(service, plaintext, context, direction, status, recipientPublicKey, senderPrivateKey, limits = TRANSPORT_BODY_LIMITS) {
|
|
978
|
+
try {
|
|
979
|
+
const bytes = new TextEncoder().encode(plaintext).byteLength;
|
|
980
|
+
if (bytes > limits.plaintextBytes)
|
|
981
|
+
return err(HaiCryptoError.TRANSPORT_PAYLOAD_TOO_LARGE, cryptoM("crypto_transportBodyTooLarge"), new TransportBodyTooLargeError(limits.plaintextBytes, bytes, { direction }));
|
|
982
|
+
const encryptionKey = service.symmetric.generateKey();
|
|
983
|
+
const authenticationKey = service.symmetric.generateKey() + service.symmetric.generateKey();
|
|
984
|
+
const encrypted = service.symmetric.encryptWithIV(plaintext, encryptionKey);
|
|
985
|
+
const wrapped = service.asymmetric.encrypt(JSON.stringify([encryptionKey, authenticationKey]), recipientPublicKey);
|
|
986
|
+
if (!encrypted.success || !wrapped.success)
|
|
987
|
+
return transportAuthenticationFailure();
|
|
988
|
+
const payload = { context: { ...context }, direction, status, encryptedKey: wrapped.data, ciphertext: encrypted.data.ciphertext, iv: encrypted.data.iv };
|
|
989
|
+
const text = authenticatedText(payload);
|
|
990
|
+
const tag = service.hash.hmac(text, authenticationKey);
|
|
991
|
+
if (!tag.success)
|
|
992
|
+
return transportAuthenticationFailure();
|
|
993
|
+
const signature = service.asymmetric.sign(JSON.stringify([text, tag.data]), senderPrivateKey);
|
|
994
|
+
if (!signature.success)
|
|
995
|
+
return transportAuthenticationFailure();
|
|
996
|
+
const authenticated = { ...payload, tag: tag.data, signature: signature.data };
|
|
997
|
+
return isValidEncryptedPayload(authenticated, limits) ? ok(authenticated) : transportAuthenticationFailure();
|
|
998
|
+
} catch {
|
|
999
|
+
return transportAuthenticationFailure();
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
function openTransportPayload(service, payload, recipientPrivateKey, senderPublicKey, limits = TRANSPORT_BODY_LIMITS) {
|
|
1003
|
+
try {
|
|
1004
|
+
if (!isValidEncryptedPayload(payload, limits))
|
|
1005
|
+
return transportAuthenticationFailure();
|
|
1006
|
+
const text = authenticatedText(payload);
|
|
1007
|
+
const verified = service.asymmetric.verify(JSON.stringify([text, payload.tag]), payload.signature, senderPublicKey);
|
|
1008
|
+
if (!verified.success || !verified.data)
|
|
1009
|
+
return transportAuthenticationFailure();
|
|
1010
|
+
const unwrapped = service.asymmetric.decrypt(payload.encryptedKey, recipientPrivateKey);
|
|
1011
|
+
if (!unwrapped.success)
|
|
1012
|
+
return transportAuthenticationFailure();
|
|
1013
|
+
const keys = JSON.parse(unwrapped.data);
|
|
1014
|
+
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])) {
|
|
1015
|
+
return transportAuthenticationFailure();
|
|
1016
|
+
}
|
|
1017
|
+
const tag = service.hash.hmac(text, keys[1]);
|
|
1018
|
+
if (!tag.success || !core.string.constantTimeEqual(tag.data, payload.tag))
|
|
1019
|
+
return transportAuthenticationFailure();
|
|
1020
|
+
const plaintext = service.symmetric.decryptWithIV(payload.ciphertext, keys[0], payload.iv);
|
|
1021
|
+
if (!plaintext.success)
|
|
1022
|
+
return transportAuthenticationFailure();
|
|
1023
|
+
const bytes = new TextEncoder().encode(plaintext.data).byteLength;
|
|
1024
|
+
if (bytes > limits.plaintextBytes)
|
|
1025
|
+
return err(HaiCryptoError.TRANSPORT_PAYLOAD_TOO_LARGE, cryptoM("crypto_transportBodyTooLarge"), new TransportBodyTooLargeError(limits.plaintextBytes, bytes, { direction: payload.direction }));
|
|
1026
|
+
return plaintext;
|
|
1027
|
+
} catch {
|
|
1028
|
+
return transportAuthenticationFailure();
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
function isValidEncryptedPayload(value, limits = TRANSPORT_BODY_LIMITS) {
|
|
1032
|
+
if (!value || typeof value !== "object")
|
|
1033
|
+
return false;
|
|
1034
|
+
const p = value;
|
|
1035
|
+
const c = p.context;
|
|
1036
|
+
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 <= 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);
|
|
1037
|
+
}
|
|
1038
|
+
function sameTransportContext(a, b) {
|
|
1039
|
+
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;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// src/transport/crypto-transport-error.ts
|
|
1043
|
+
var TransportTimeError = class extends Error {
|
|
1044
|
+
constructor(context) {
|
|
1045
|
+
super(cryptoM("crypto_transportTimeInvalid"));
|
|
1046
|
+
this.context = context;
|
|
1047
|
+
this.name = "TransportTimeError";
|
|
1048
|
+
}
|
|
1049
|
+
context;
|
|
1050
|
+
code = HaiCryptoError.TRANSPORT_TIME_INVALID.code;
|
|
1051
|
+
};
|
|
1052
|
+
async function createTransportErrorResponse(error, authenticated) {
|
|
1053
|
+
const status = error instanceof TransportTimeError ? 400 : error.direction === "request" ? 413 : 500;
|
|
1054
|
+
const body = JSON.stringify({ success: false, error: { code: error.code, message: error.message, httpStatus: status, details: error instanceof TransportTimeError ? void 0 : {
|
|
1055
|
+
direction: error.direction,
|
|
1056
|
+
stage: error.stage,
|
|
1057
|
+
limitBytes: error.limitBytes,
|
|
1058
|
+
actualBytes: error.actualBytes
|
|
1059
|
+
} } });
|
|
1060
|
+
if (authenticated) {
|
|
1061
|
+
const encrypted = await authenticated.manager.encryptResponse(authenticated.context, body, status);
|
|
1062
|
+
if (encrypted.success) {
|
|
1063
|
+
return Response.json(encrypted.data, { status, headers: { [TRANSPORT_PROTOCOL.ENCRYPTED_HEADER]: TRANSPORT_PROTOCOL.ENCRYPTED_HEADER_VALUE } });
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
return new Response(body, { status, headers: { "Content-Type": "application/json; charset=utf-8" } });
|
|
1067
|
+
}
|
|
1068
|
+
|
|
784
1069
|
// src/transport/crypto-transport-client.ts
|
|
785
1070
|
function createTransportClient(options) {
|
|
786
1071
|
const baseFetch = options.fetch ?? fetch;
|
|
1072
|
+
const localLimits = options.maxPlaintextBytes === void 0 ? void 0 : resolveTransportBodyLimits(options.maxPlaintextBytes);
|
|
1073
|
+
let bodyLimits = localLimits ?? resolveTransportBodyLimits();
|
|
787
1074
|
let keyPair = null;
|
|
788
1075
|
let serverPublicKey = null;
|
|
1076
|
+
let serverKeyId = null;
|
|
789
1077
|
let clientId = null;
|
|
790
1078
|
let initPromise = null;
|
|
791
1079
|
async function doInit() {
|
|
@@ -793,30 +1081,53 @@ function createTransportClient(options) {
|
|
|
793
1081
|
if (!kpResult.success)
|
|
794
1082
|
return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportClientKeyGenerateFailed"), kpResult.error);
|
|
795
1083
|
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;
|
|
1084
|
+
if (new TextEncoder().encode(localKeyPair.publicKey).byteLength > TRANSPORT_BODY_LIMITS.publicKeyBytes)
|
|
1085
|
+
return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportClientKeyGenerateFailed"));
|
|
1086
|
+
const controller = new AbortController();
|
|
1087
|
+
const timeout = setTimeout(() => controller.abort(new DOMException("Key exchange timed out", "TimeoutError")), 3e4);
|
|
809
1088
|
try {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
1089
|
+
let response;
|
|
1090
|
+
try {
|
|
1091
|
+
response = await baseFetch(options.keyExchangeUrl, {
|
|
1092
|
+
method: "POST",
|
|
1093
|
+
headers: { "Content-Type": "application/json" },
|
|
1094
|
+
body: JSON.stringify({ clientPublicKey: localKeyPair.publicKey }),
|
|
1095
|
+
signal: controller.signal
|
|
1096
|
+
});
|
|
1097
|
+
} catch (cause) {
|
|
1098
|
+
return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportKeyExchangeRequestFailed"), cause);
|
|
1099
|
+
}
|
|
1100
|
+
if (!response.ok) {
|
|
1101
|
+
void response.body?.cancel().catch(() => {
|
|
1102
|
+
});
|
|
1103
|
+
return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportKeyExchangeHttpFailed", { params: { status: response.status } }));
|
|
1104
|
+
}
|
|
1105
|
+
let body;
|
|
1106
|
+
try {
|
|
1107
|
+
body = JSON.parse(await readTransportBody(response.body, {
|
|
1108
|
+
maxBytes: TRANSPORT_BODY_LIMITS.keyExchangeBytes,
|
|
1109
|
+
signal: controller.signal
|
|
1110
|
+
}));
|
|
1111
|
+
} catch {
|
|
1112
|
+
return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportKeyExchangeInvalidJson"));
|
|
1113
|
+
}
|
|
1114
|
+
if (!isKeyExchangeResponse(body)) {
|
|
1115
|
+
return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportKeyExchangeMissingFields"));
|
|
1116
|
+
}
|
|
1117
|
+
try {
|
|
1118
|
+
const serverLimits = resolveTransportBodyLimits(body.maxPlaintextBytes);
|
|
1119
|
+
bodyLimits = resolveTransportBodyLimits(Math.min(localLimits?.plaintextBytes ?? serverLimits.plaintextBytes, serverLimits.plaintextBytes));
|
|
1120
|
+
} catch {
|
|
1121
|
+
return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportBodyLimitInvalid"));
|
|
1122
|
+
}
|
|
1123
|
+
keyPair = localKeyPair;
|
|
1124
|
+
serverPublicKey = body.serverPublicKey;
|
|
1125
|
+
serverKeyId = body.serverKeyId;
|
|
1126
|
+
clientId = body.clientId;
|
|
1127
|
+
return ok(void 0);
|
|
1128
|
+
} finally {
|
|
1129
|
+
clearTimeout(timeout);
|
|
813
1130
|
}
|
|
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
1131
|
}
|
|
821
1132
|
async function ensureReady() {
|
|
822
1133
|
if (clientId && serverPublicKey)
|
|
@@ -830,66 +1141,91 @@ function createTransportClient(options) {
|
|
|
830
1141
|
}
|
|
831
1142
|
return initPromise;
|
|
832
1143
|
}
|
|
833
|
-
function encryptBody(plaintext) {
|
|
834
|
-
if (!serverPublicKey)
|
|
1144
|
+
function encryptBody(plaintext, context) {
|
|
1145
|
+
if (!serverPublicKey || !serverKeyId || !keyPair)
|
|
835
1146
|
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
|
-
});
|
|
1147
|
+
return sealTransportPayload(options.crypto, plaintext, context, "request", 0, serverPublicKey, keyPair.privateKey, bodyLimits);
|
|
848
1148
|
}
|
|
849
1149
|
function decryptPayload(payload) {
|
|
850
|
-
if (!keyPair)
|
|
1150
|
+
if (!keyPair || !serverPublicKey)
|
|
851
1151
|
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);
|
|
1152
|
+
return openTransportPayload(options.crypto, payload, keyPair.privateKey, serverPublicKey, bodyLimits);
|
|
859
1153
|
}
|
|
860
1154
|
const encryptedFetch = async (input, init) => {
|
|
861
1155
|
const requestInput = isRequestLike(input) ? input : void 0;
|
|
1156
|
+
const signal = init?.signal ?? requestInput?.signal ?? void 0;
|
|
1157
|
+
signal?.throwIfAborted();
|
|
862
1158
|
const targetUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : requestInput?.url ?? String(input);
|
|
863
1159
|
if (targetUrl === options.keyExchangeUrl)
|
|
864
1160
|
return baseFetch(input, init);
|
|
865
|
-
|
|
1161
|
+
let onAbort;
|
|
1162
|
+
let ready;
|
|
1163
|
+
try {
|
|
1164
|
+
ready = await (signal ? Promise.race([
|
|
1165
|
+
ensureReady(),
|
|
1166
|
+
new Promise((_resolve, reject) => {
|
|
1167
|
+
onAbort = () => reject(signal.reason);
|
|
1168
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1169
|
+
if (signal.aborted)
|
|
1170
|
+
onAbort();
|
|
1171
|
+
})
|
|
1172
|
+
]) : ensureReady());
|
|
1173
|
+
} finally {
|
|
1174
|
+
if (onAbort)
|
|
1175
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1176
|
+
}
|
|
866
1177
|
if (!ready.success)
|
|
867
1178
|
throw new Error(ready.error.message);
|
|
868
1179
|
const headers = new Headers(init?.headers ?? requestInput?.headers);
|
|
869
1180
|
headers.set(TRANSPORT_PROTOCOL.CLIENT_ID_HEADER, clientId);
|
|
870
1181
|
let rawBody = init?.body;
|
|
871
1182
|
if (rawBody == null && requestInput) {
|
|
872
|
-
const
|
|
873
|
-
if (
|
|
874
|
-
const text = await requestInput.
|
|
1183
|
+
const method2 = (init?.method ?? requestInput.method).toUpperCase();
|
|
1184
|
+
if (method2 !== "GET" && method2 !== "HEAD") {
|
|
1185
|
+
const text = await readTransportBody(requestInput.body, { maxBytes: bodyLimits.plaintextBytes, signal });
|
|
875
1186
|
if (text)
|
|
876
1187
|
rawBody = text;
|
|
877
1188
|
}
|
|
878
1189
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1190
|
+
const method = (init?.method ?? requestInput?.method ?? "GET").toUpperCase();
|
|
1191
|
+
const base = new URL(options.keyExchangeUrl, globalThis.location?.href ?? "http://localhost");
|
|
1192
|
+
const url = new URL(targetUrl, base);
|
|
1193
|
+
const pathHash = hashTransportPath(options.crypto, url.pathname + url.search);
|
|
1194
|
+
if (!pathHash.success)
|
|
1195
|
+
throw new Error(pathHash.error.message);
|
|
1196
|
+
const context = {
|
|
1197
|
+
version: TRANSPORT_PROTOCOL.VERSION,
|
|
1198
|
+
serverKeyId,
|
|
1199
|
+
clientId,
|
|
1200
|
+
requestId: options.crypto.symmetric.generateKey(),
|
|
1201
|
+
issuedAt: Date.now(),
|
|
1202
|
+
method,
|
|
1203
|
+
pathHash: pathHash.data
|
|
1204
|
+
};
|
|
1205
|
+
const plaintext = rawBody == null ? "" : await readTransportBody(new Response(rawBody).body, { maxBytes: bodyLimits.plaintextBytes, signal });
|
|
1206
|
+
const encResult = encryptBody(plaintext, context);
|
|
1207
|
+
if (!encResult.success)
|
|
1208
|
+
throw new Error(encResult.error.message);
|
|
1209
|
+
const serialized = JSON.stringify(encResult.data);
|
|
1210
|
+
if (new TextEncoder().encode(serialized).byteLength > bodyLimits.encryptedPayloadBytes)
|
|
1211
|
+
throw new TransportBodyTooLargeError(bodyLimits.encryptedPayloadBytes, new TextEncoder().encode(serialized).byteLength, { stage: "encrypted" });
|
|
1212
|
+
let nextBody;
|
|
1213
|
+
headers.delete(TRANSPORT_PROTOCOL.ENVELOPE_HEADER);
|
|
1214
|
+
headers.set(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER, TRANSPORT_PROTOCOL.ENCRYPTED_HEADER_VALUE);
|
|
1215
|
+
headers.delete("Content-Length");
|
|
1216
|
+
headers.delete("Content-Encoding");
|
|
1217
|
+
if (method === "GET" || method === "HEAD") {
|
|
1218
|
+
if (new TextEncoder().encode(serialized).byteLength > TRANSPORT_BODY_LIMITS.envelopeHeaderBytes)
|
|
1219
|
+
throw new TransportBodyTooLargeError(TRANSPORT_BODY_LIMITS.envelopeHeaderBytes, new TextEncoder().encode(serialized).byteLength, { stage: "header" });
|
|
1220
|
+
headers.set(TRANSPORT_PROTOCOL.ENVELOPE_HEADER, serialized);
|
|
1221
|
+
} else {
|
|
1222
|
+
nextBody = serialized;
|
|
886
1223
|
headers.set("Content-Type", "application/json");
|
|
887
|
-
headers.set(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER, TRANSPORT_PROTOCOL.ENCRYPTED_HEADER_VALUE);
|
|
888
1224
|
}
|
|
889
1225
|
const finalInput = requestInput ? requestInput.url : input;
|
|
890
1226
|
const finalInit = {
|
|
891
1227
|
...init,
|
|
892
|
-
method
|
|
1228
|
+
method,
|
|
893
1229
|
headers,
|
|
894
1230
|
body: nextBody,
|
|
895
1231
|
cache: init?.cache ?? requestInput?.cache,
|
|
@@ -903,24 +1239,66 @@ function createTransportClient(options) {
|
|
|
903
1239
|
signal: init?.signal ?? requestInput?.signal
|
|
904
1240
|
};
|
|
905
1241
|
const response = await baseFetch(finalInput, finalInit);
|
|
906
|
-
if (response.headers.get(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER) !== TRANSPORT_PROTOCOL.ENCRYPTED_HEADER_VALUE)
|
|
907
|
-
|
|
908
|
-
|
|
1242
|
+
if (response.headers.get(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER) !== TRANSPORT_PROTOCOL.ENCRYPTED_HEADER_VALUE) {
|
|
1243
|
+
void response.body?.cancel().catch(() => {
|
|
1244
|
+
});
|
|
1245
|
+
if (response.status === 413)
|
|
1246
|
+
throw new TransportBodyTooLargeError(bodyLimits.encryptedPayloadBytes, void 0, { stage: "encrypted" });
|
|
1247
|
+
throw new Error(cryptoM("crypto_transportResponseDecryptFailed"));
|
|
1248
|
+
}
|
|
909
1249
|
let payload;
|
|
910
1250
|
try {
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1251
|
+
const envelope = response.headers.get(TRANSPORT_PROTOCOL.ENVELOPE_HEADER);
|
|
1252
|
+
if (envelope && response.body !== null) {
|
|
1253
|
+
void response.body.cancel().catch(() => {
|
|
1254
|
+
});
|
|
1255
|
+
throw new Error(cryptoM("crypto_transportResponseDecryptFailed"));
|
|
1256
|
+
}
|
|
1257
|
+
if (envelope && new TextEncoder().encode(envelope).byteLength > TRANSPORT_BODY_LIMITS.envelopeHeaderBytes)
|
|
1258
|
+
throw new TransportBodyTooLargeError(TRANSPORT_BODY_LIMITS.envelopeHeaderBytes, new TextEncoder().encode(envelope).byteLength, { direction: "response", stage: "header" });
|
|
1259
|
+
payload = JSON.parse(envelope ?? await readTransportBody(response.body, { maxBytes: bodyLimits.encryptedPayloadBytes, direction: "response", stage: "encrypted", signal }));
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
if (error instanceof TransportBodyTooLargeError)
|
|
1262
|
+
throw error;
|
|
1263
|
+
throw new Error(cryptoM("crypto_transportResponseDecryptFailed"), signal?.aborted ? { cause: signal.reason } : void 0);
|
|
1264
|
+
}
|
|
1265
|
+
if (!isValidEncryptedPayload(payload, bodyLimits) || payload.direction !== "response" || payload.status !== response.status || !sameTransportContext(payload.context, context)) {
|
|
1266
|
+
throw new Error(cryptoM("crypto_transportResponseDecryptFailed"));
|
|
914
1267
|
}
|
|
915
|
-
if (!isEncryptedPayload(payload))
|
|
916
|
-
return response;
|
|
917
1268
|
const decResult = decryptPayload(payload);
|
|
918
|
-
if (!decResult.success)
|
|
1269
|
+
if (!decResult.success) {
|
|
1270
|
+
if (decResult.error.cause instanceof TransportBodyTooLargeError)
|
|
1271
|
+
throw decResult.error.cause;
|
|
919
1272
|
throw new Error(decResult.error.message);
|
|
1273
|
+
}
|
|
1274
|
+
if (new TextEncoder().encode(decResult.data).byteLength > bodyLimits.plaintextBytes)
|
|
1275
|
+
throw new TransportBodyTooLargeError(bodyLimits.plaintextBytes, new TextEncoder().encode(decResult.data).byteLength, { direction: "response" });
|
|
1276
|
+
if (response.status === 400) {
|
|
1277
|
+
let failure;
|
|
1278
|
+
try {
|
|
1279
|
+
failure = JSON.parse(decResult.data);
|
|
1280
|
+
} catch {
|
|
1281
|
+
}
|
|
1282
|
+
if (failure?.success === false && failure.error?.code === HaiCryptoError.TRANSPORT_TIME_INVALID.code)
|
|
1283
|
+
throw new TransportTimeError();
|
|
1284
|
+
}
|
|
1285
|
+
if (response.status === 500) {
|
|
1286
|
+
let failure;
|
|
1287
|
+
try {
|
|
1288
|
+
failure = JSON.parse(decResult.data);
|
|
1289
|
+
} catch {
|
|
1290
|
+
}
|
|
1291
|
+
const details = failure?.error?.details;
|
|
1292
|
+
if (failure?.success === false && failure.error?.code === "hai:crypto:070" && details?.direction === "response" && (details.stage === "plaintext" || details.stage === "encrypted" || details.stage === "header") && typeof details.limitBytes === "number" && Number.isSafeInteger(details.limitBytes) && details.limitBytes >= 0) {
|
|
1293
|
+
throw new TransportBodyTooLargeError(details.limitBytes, typeof details.actualBytes === "number" ? details.actualBytes : void 0, { direction: "response", stage: details.stage });
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
920
1296
|
const respHeaders = new Headers(response.headers);
|
|
921
1297
|
respHeaders.delete(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER);
|
|
1298
|
+
respHeaders.delete(TRANSPORT_PROTOCOL.ENVELOPE_HEADER);
|
|
922
1299
|
respHeaders.delete("Content-Length");
|
|
923
|
-
|
|
1300
|
+
respHeaders.delete("Content-Encoding");
|
|
1301
|
+
return new Response(method === "HEAD" || [204, 304].includes(response.status) ? null : decResult.data, {
|
|
924
1302
|
status: response.status,
|
|
925
1303
|
statusText: response.statusText,
|
|
926
1304
|
headers: respHeaders
|
|
@@ -933,24 +1311,12 @@ function createTransportClient(options) {
|
|
|
933
1311
|
destroy() {
|
|
934
1312
|
keyPair = null;
|
|
935
1313
|
serverPublicKey = null;
|
|
1314
|
+
serverKeyId = null;
|
|
936
1315
|
clientId = null;
|
|
937
1316
|
initPromise = null;
|
|
938
1317
|
}
|
|
939
1318
|
};
|
|
940
1319
|
}
|
|
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
1320
|
function isRequestLike(input) {
|
|
955
1321
|
if (typeof input !== "object" || input === null)
|
|
956
1322
|
return false;
|
|
@@ -958,95 +1324,130 @@ function isRequestLike(input) {
|
|
|
958
1324
|
const headers = candidate.headers;
|
|
959
1325
|
return typeof candidate.url === "string" && typeof headers?.get === "function" && typeof candidate.method === "string" && typeof candidate.clone === "function";
|
|
960
1326
|
}
|
|
961
|
-
function
|
|
962
|
-
if (!
|
|
1327
|
+
function isKeyExchangeResponse(body) {
|
|
1328
|
+
if (!body || typeof body !== "object")
|
|
963
1329
|
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));
|
|
1330
|
+
const fields = body;
|
|
1331
|
+
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
1332
|
}
|
|
975
|
-
function
|
|
976
|
-
|
|
977
|
-
|
|
1333
|
+
function parseTransportServerKeyRing(serialized) {
|
|
1334
|
+
try {
|
|
1335
|
+
const value = typeof serialized === "string" ? JSON.parse(serialized) : serialized;
|
|
1336
|
+
if (isTransportServerKeyRing(value))
|
|
1337
|
+
return ok(value);
|
|
1338
|
+
} catch {
|
|
1339
|
+
}
|
|
1340
|
+
return err(HaiCryptoError.INVALID_KEY, cryptoM("crypto_transportServerKeyRingInvalid"));
|
|
978
1341
|
}
|
|
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
|
-
};
|
|
1342
|
+
function isTransportServerKeyRing(value) {
|
|
1343
|
+
if (!value || typeof value !== "object")
|
|
1344
|
+
return false;
|
|
1345
|
+
const ring = value;
|
|
1346
|
+
if (typeof ring.activeKeyId !== "string" || !/^[\w-]{1,64}$/.test(ring.activeKeyId) || !ring.keys || typeof ring.keys !== "object" || Array.isArray(ring.keys)) {
|
|
1347
|
+
return false;
|
|
1348
|
+
}
|
|
1349
|
+
if (!Object.hasOwn(ring.keys, ring.activeKeyId))
|
|
1350
|
+
return false;
|
|
1351
|
+
return Object.entries(ring.keys).every(([id, pair]) => {
|
|
1352
|
+
if (!/^[\w-]{1,64}$/.test(id) || !pair || typeof pair !== "object")
|
|
1353
|
+
return false;
|
|
1354
|
+
const fields = pair;
|
|
1355
|
+
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;
|
|
1356
|
+
});
|
|
1005
1357
|
}
|
|
1006
1358
|
|
|
1007
1359
|
// src/transport/crypto-transport-server.ts
|
|
1008
|
-
function createTransportEncryption(cryptoService, options
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1360
|
+
function createTransportEncryption(cryptoService, options) {
|
|
1361
|
+
let bodyLimits;
|
|
1362
|
+
try {
|
|
1363
|
+
bodyLimits = resolveTransportBodyLimits(options?.maxPlaintextBytes);
|
|
1364
|
+
} catch {
|
|
1365
|
+
return err(HaiCryptoError.INVALID_INPUT, cryptoM("crypto_transportBodyLimitInvalid"));
|
|
1366
|
+
}
|
|
1367
|
+
const ring = parseTransportServerKeyRing(options?.serverKeyRing);
|
|
1368
|
+
if (!ring.success)
|
|
1369
|
+
return ring;
|
|
1370
|
+
if (!options.keyStore || typeof options.keyStore.register !== "function" || typeof options.keyStore.get !== "function" || typeof options.keyStore.claimRequest !== "function") {
|
|
1371
|
+
return err(HaiCryptoError.INVALID_INPUT, cryptoM("crypto_transportKeyStoreRequired"));
|
|
1372
|
+
}
|
|
1373
|
+
if (core.module.requiresSharedState(globalThis.process?.env ?? {}) && options.keyStore.stateScope !== "shared")
|
|
1374
|
+
return err(HaiCryptoError.INVALID_INPUT, cryptoM("crypto_transportKeyStoreRequired"));
|
|
1375
|
+
const serverKeys = /* @__PURE__ */ new Map();
|
|
1376
|
+
for (const [id, key] of Object.entries(ring.data.keys)) {
|
|
1377
|
+
const probe = cryptoService.asymmetric.encrypt("hai-transport-key-check", key.publicKey);
|
|
1378
|
+
if (!probe.success)
|
|
1379
|
+
return err(HaiCryptoError.INVALID_KEY, cryptoM("crypto_transportServerKeyRingInvalid"));
|
|
1380
|
+
const checked = cryptoService.asymmetric.decrypt(probe.data, key.privateKey);
|
|
1381
|
+
if (!checked.success || checked.data !== "hai-transport-key-check")
|
|
1382
|
+
return err(HaiCryptoError.INVALID_KEY, cryptoM("crypto_transportServerKeyRingInvalid"));
|
|
1383
|
+
serverKeys.set(id, { publicKey: key.publicKey, privateKey: key.privateKey });
|
|
1384
|
+
}
|
|
1385
|
+
const activeKeyId = ring.data.activeKeyId;
|
|
1386
|
+
const serverKeyPair = serverKeys.get(activeKeyId);
|
|
1387
|
+
const keyStore = options.keyStore;
|
|
1014
1388
|
const manager = {
|
|
1389
|
+
bodyLimits,
|
|
1390
|
+
getServerKeyId() {
|
|
1391
|
+
return activeKeyId;
|
|
1392
|
+
},
|
|
1015
1393
|
getServerPublicKey() {
|
|
1016
1394
|
return serverKeyPair.publicKey;
|
|
1017
1395
|
},
|
|
1018
1396
|
async registerClientKey(clientPublicKey) {
|
|
1397
|
+
if (!isValidSM2PublicKey(clientPublicKey))
|
|
1398
|
+
throw new TypeError(cryptoM("crypto_sm2PublicKeyInvalid"));
|
|
1019
1399
|
return keyStore.register(clientPublicKey);
|
|
1020
1400
|
},
|
|
1021
1401
|
async getClientPublicKey(clientId) {
|
|
1022
1402
|
return keyStore.get(clientId);
|
|
1023
1403
|
},
|
|
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);
|
|
1404
|
+
async encryptResponse(request, data, status) {
|
|
1405
|
+
try {
|
|
1406
|
+
const context = { ...request };
|
|
1407
|
+
const clientPublicKey = await keyStore.get(context.clientId);
|
|
1408
|
+
if (!clientPublicKey)
|
|
1409
|
+
return err(HaiCommonError.NOT_FOUND, cryptoM("crypto_transportClientNotRegistered"));
|
|
1410
|
+
const key = serverKeys.get(context.serverKeyId);
|
|
1411
|
+
if (!key)
|
|
1412
|
+
return transportAuthenticationFailure();
|
|
1413
|
+
return sealTransportPayload(cryptoService, data, context, "response", status, clientPublicKey, key.privateKey, bodyLimits);
|
|
1414
|
+
} catch {
|
|
1415
|
+
return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportResponseEncryptFailed"));
|
|
1416
|
+
}
|
|
1041
1417
|
},
|
|
1042
|
-
decryptRequest(payload) {
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1418
|
+
async decryptRequest(payload, expected) {
|
|
1419
|
+
try {
|
|
1420
|
+
if (!isValidEncryptedPayload(payload, bodyLimits) || payload.direction !== "request" || payload.status !== 0)
|
|
1421
|
+
return transportAuthenticationFailure();
|
|
1422
|
+
const authenticated = { ...payload, context: { ...payload.context } };
|
|
1423
|
+
const c = authenticated.context;
|
|
1424
|
+
const pathHash = hashTransportPath(cryptoService, expected.path);
|
|
1425
|
+
if (!pathHash.success || c.clientId !== expected.clientId || c.method !== expected.method || c.pathHash !== pathHash.data) {
|
|
1426
|
+
return transportAuthenticationFailure();
|
|
1427
|
+
}
|
|
1428
|
+
const key = serverKeys.get(c.serverKeyId);
|
|
1429
|
+
const clientPublicKey = await keyStore.get(c.clientId);
|
|
1430
|
+
if (!key || !clientPublicKey)
|
|
1431
|
+
return transportAuthenticationFailure();
|
|
1432
|
+
const plaintext = openTransportPayload(cryptoService, authenticated, key.privateKey, clientPublicKey, bodyLimits);
|
|
1433
|
+
if (!plaintext.success)
|
|
1434
|
+
return plaintext;
|
|
1435
|
+
const timeFailure = () => {
|
|
1436
|
+
const error = new TransportTimeError(c);
|
|
1437
|
+
return err(HaiCryptoError.TRANSPORT_TIME_INVALID, error.message, error);
|
|
1438
|
+
};
|
|
1439
|
+
const now = Date.now();
|
|
1440
|
+
if (c.issuedAt > now + TRANSPORT_PROTOCOL.CLOCK_SKEW_MS || now >= c.issuedAt + TRANSPORT_PROTOCOL.REQUEST_WINDOW_MS)
|
|
1441
|
+
return timeFailure();
|
|
1442
|
+
if (!await keyStore.claimRequest(c.clientId, c.requestId, c.issuedAt + TRANSPORT_PROTOCOL.REQUEST_WINDOW_MS + 2 * TRANSPORT_PROTOCOL.CLOCK_SKEW_MS)) {
|
|
1443
|
+
return transportAuthenticationFailure();
|
|
1444
|
+
}
|
|
1445
|
+
if (Date.now() >= c.issuedAt + TRANSPORT_PROTOCOL.REQUEST_WINDOW_MS)
|
|
1446
|
+
return timeFailure();
|
|
1447
|
+
return plaintext;
|
|
1448
|
+
} catch {
|
|
1449
|
+
return transportAuthenticationFailure();
|
|
1450
|
+
}
|
|
1050
1451
|
},
|
|
1051
1452
|
async close() {
|
|
1052
1453
|
await keyStore.close?.();
|
|
@@ -1075,7 +1476,7 @@ function getCrypto() {
|
|
|
1075
1476
|
return crypto2;
|
|
1076
1477
|
}
|
|
1077
1478
|
var transportOperations = {
|
|
1078
|
-
createServer: (options) => createTransportEncryption(getCrypto(), options),
|
|
1479
|
+
createServer: (options) => getCrypto().isInitialized ? createTransportEncryption(getCrypto(), options) : notInitialized.result(),
|
|
1079
1480
|
createClient: (options) => createTransportClient({ crypto: getCrypto(), ...options }),
|
|
1080
1481
|
protocol: TRANSPORT_PROTOCOL
|
|
1081
1482
|
};
|
|
@@ -1148,8 +1549,7 @@ var crypto2 = {
|
|
|
1148
1549
|
/**
|
|
1149
1550
|
* 传输加密操作(端到端混合加密)
|
|
1150
1551
|
*
|
|
1151
|
-
* createServer
|
|
1152
|
-
* 自动通过 asymmetric proxy 返回 NOT_INITIALIZED 错误。
|
|
1552
|
+
* createServer 检查初始化后复用 asymmetric/symmetric;未初始化返回 NOT_INITIALIZED。
|
|
1153
1553
|
*/
|
|
1154
1554
|
transport: transportOperations,
|
|
1155
1555
|
/** 是否已初始化 */
|
|
@@ -1175,13 +1575,120 @@ var crypto2 = {
|
|
|
1175
1575
|
logger.info("Crypto module closed");
|
|
1176
1576
|
}
|
|
1177
1577
|
};
|
|
1578
|
+
async function authenticateTransportRequest(manager, request) {
|
|
1579
|
+
const reject = () => {
|
|
1580
|
+
void request.body?.cancel().catch(() => {
|
|
1581
|
+
});
|
|
1582
|
+
return transportAuthenticationFailure();
|
|
1583
|
+
};
|
|
1584
|
+
if (request.headers.get(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER) !== TRANSPORT_PROTOCOL.ENCRYPTED_HEADER_VALUE)
|
|
1585
|
+
return reject();
|
|
1586
|
+
const clientId = request.headers.get(TRANSPORT_PROTOCOL.CLIENT_ID_HEADER);
|
|
1587
|
+
if (!clientId)
|
|
1588
|
+
return reject();
|
|
1589
|
+
const envelope = request.headers.get(TRANSPORT_PROTOCOL.ENVELOPE_HEADER);
|
|
1590
|
+
const bodyless = request.method === "GET" || request.method === "HEAD";
|
|
1591
|
+
if (bodyless && (!envelope || request.body !== null) || !bodyless && envelope !== null)
|
|
1592
|
+
return reject();
|
|
1593
|
+
if (envelope && new TextEncoder().encode(envelope).byteLength > TRANSPORT_BODY_LIMITS.envelopeHeaderBytes)
|
|
1594
|
+
throw new TransportBodyTooLargeError(TRANSPORT_BODY_LIMITS.envelopeHeaderBytes, new TextEncoder().encode(envelope).byteLength, { stage: "header" });
|
|
1595
|
+
const text = envelope ?? await readTransportBody(request.body, {
|
|
1596
|
+
maxBytes: manager.bodyLimits.encryptedPayloadBytes,
|
|
1597
|
+
stage: "encrypted",
|
|
1598
|
+
signal: request.signal
|
|
1599
|
+
});
|
|
1600
|
+
let payload;
|
|
1601
|
+
try {
|
|
1602
|
+
payload = JSON.parse(text);
|
|
1603
|
+
} catch {
|
|
1604
|
+
return transportAuthenticationFailure();
|
|
1605
|
+
}
|
|
1606
|
+
if (!isValidEncryptedPayload(payload, manager.bodyLimits) || payload.direction !== "request" || payload.status !== 0)
|
|
1607
|
+
return transportAuthenticationFailure();
|
|
1608
|
+
const url = new URL(request.url);
|
|
1609
|
+
const decrypted = await manager.decryptRequest(payload, {
|
|
1610
|
+
clientId,
|
|
1611
|
+
method: request.method,
|
|
1612
|
+
path: url.pathname + url.search
|
|
1613
|
+
});
|
|
1614
|
+
if (!decrypted.success) {
|
|
1615
|
+
if (decrypted.error.cause instanceof TransportBodyTooLargeError)
|
|
1616
|
+
throw decrypted.error.cause;
|
|
1617
|
+
return decrypted;
|
|
1618
|
+
}
|
|
1619
|
+
if (new TextEncoder().encode(decrypted.data).byteLength > manager.bodyLimits.plaintextBytes)
|
|
1620
|
+
throw new TransportBodyTooLargeError(manager.bodyLimits.plaintextBytes, new TextEncoder().encode(decrypted.data).byteLength);
|
|
1621
|
+
if (bodyless && decrypted.data !== "")
|
|
1622
|
+
return transportAuthenticationFailure();
|
|
1623
|
+
return ok({ plaintext: decrypted.data, context: payload.context });
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// src/transport/store-provider/crypto-transport-store-memory.ts
|
|
1627
|
+
var DEFAULT_MAX_CLIENTS = 1e4;
|
|
1628
|
+
function normalizeMaxClients(maxClients) {
|
|
1629
|
+
if (!Number.isFinite(maxClients))
|
|
1630
|
+
return DEFAULT_MAX_CLIENTS;
|
|
1631
|
+
return Math.max(1, Math.floor(maxClients));
|
|
1632
|
+
}
|
|
1633
|
+
function createClientId(counter) {
|
|
1634
|
+
const entropy = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID().replace(/-/g, "") : Math.random().toString(36).slice(2, 12);
|
|
1635
|
+
return `c_${counter}_${Date.now()}_${entropy}`;
|
|
1636
|
+
}
|
|
1637
|
+
function createInMemoryKeyStore(maxClients = DEFAULT_MAX_CLIENTS) {
|
|
1638
|
+
const clientKeys = /* @__PURE__ */ new Map();
|
|
1639
|
+
const requests = /* @__PURE__ */ new Map();
|
|
1640
|
+
const capacity = normalizeMaxClients(maxClients);
|
|
1641
|
+
let counter = 0;
|
|
1642
|
+
return {
|
|
1643
|
+
get stateScope() {
|
|
1644
|
+
return "local";
|
|
1645
|
+
},
|
|
1646
|
+
async register(publicKey) {
|
|
1647
|
+
if (!isValidSM2PublicKey(publicKey))
|
|
1648
|
+
throw new TypeError(cryptoM("crypto_sm2PublicKeyInvalid"));
|
|
1649
|
+
counter++;
|
|
1650
|
+
const clientId = createClientId(counter);
|
|
1651
|
+
if (clientKeys.size >= capacity) {
|
|
1652
|
+
const oldest = clientKeys.keys().next().value;
|
|
1653
|
+
if (oldest !== void 0)
|
|
1654
|
+
clientKeys.delete(oldest);
|
|
1655
|
+
}
|
|
1656
|
+
clientKeys.set(clientId, publicKey);
|
|
1657
|
+
return clientId;
|
|
1658
|
+
},
|
|
1659
|
+
async get(clientId) {
|
|
1660
|
+
return clientKeys.get(clientId);
|
|
1661
|
+
},
|
|
1662
|
+
async claimRequest(clientId, requestId, expiresAt) {
|
|
1663
|
+
const now = Date.now();
|
|
1664
|
+
for (const [id2, expiry] of requests) {
|
|
1665
|
+
if (expiry <= now)
|
|
1666
|
+
requests.delete(id2);
|
|
1667
|
+
}
|
|
1668
|
+
const id = JSON.stringify([clientId, requestId]);
|
|
1669
|
+
if (requests.has(id) || expiresAt <= now)
|
|
1670
|
+
return false;
|
|
1671
|
+
requests.set(id, expiresAt);
|
|
1672
|
+
return true;
|
|
1673
|
+
},
|
|
1674
|
+
async delete(clientId) {
|
|
1675
|
+
clientKeys.delete(clientId);
|
|
1676
|
+
},
|
|
1677
|
+
async close() {
|
|
1678
|
+
clientKeys.clear();
|
|
1679
|
+
requests.clear();
|
|
1680
|
+
}
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1178
1683
|
|
|
1179
1684
|
// src/transport/store-provider/crypto-transport-store-redis.ts
|
|
1180
1685
|
var TRANSPORT_KEY_CACHE_PREFIX = "hai:crypto:transport:client";
|
|
1181
1686
|
function normalizeTtlSeconds(ttlSeconds) {
|
|
1182
|
-
if (ttlSeconds === void 0
|
|
1687
|
+
if (ttlSeconds === void 0)
|
|
1183
1688
|
return void 0;
|
|
1184
|
-
|
|
1689
|
+
if (!Number.isSafeInteger(ttlSeconds) || ttlSeconds <= 0 || ttlSeconds > 2147483647)
|
|
1690
|
+
throw new TypeError(cryptoM("crypto_transportKeyStoreTtlInvalid"));
|
|
1691
|
+
return ttlSeconds;
|
|
1185
1692
|
}
|
|
1186
1693
|
function createClientId2() {
|
|
1187
1694
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function")
|
|
@@ -1204,9 +1711,14 @@ function createRedisTransportKeyStore(options) {
|
|
|
1204
1711
|
return `${TRANSPORT_KEY_CACHE_PREFIX}:${clientId}`;
|
|
1205
1712
|
}
|
|
1206
1713
|
return {
|
|
1714
|
+
get stateScope() {
|
|
1715
|
+
return options.cache.config?.type === "redis" ? "shared" : "local";
|
|
1716
|
+
},
|
|
1207
1717
|
async register(publicKey) {
|
|
1718
|
+
if (!isValidSM2PublicKey(publicKey))
|
|
1719
|
+
throw new TypeError(cryptoM("crypto_sm2PublicKeyInvalid"));
|
|
1208
1720
|
const clientId = createClientId2();
|
|
1209
|
-
unwrapResult(
|
|
1721
|
+
const applied = unwrapResult(
|
|
1210
1722
|
"cache.kv.set",
|
|
1211
1723
|
await options.cache.kv.set(
|
|
1212
1724
|
buildCacheKey(clientId),
|
|
@@ -1214,6 +1726,8 @@ function createRedisTransportKeyStore(options) {
|
|
|
1214
1726
|
ttlSeconds === void 0 ? void 0 : { ex: ttlSeconds }
|
|
1215
1727
|
)
|
|
1216
1728
|
);
|
|
1729
|
+
if (!applied)
|
|
1730
|
+
throw new Error(cryptoM("crypto_transportClientNotRegistered"));
|
|
1217
1731
|
return clientId;
|
|
1218
1732
|
},
|
|
1219
1733
|
async get(clientId) {
|
|
@@ -1223,6 +1737,17 @@ function createRedisTransportKeyStore(options) {
|
|
|
1223
1737
|
);
|
|
1224
1738
|
return publicKey ?? void 0;
|
|
1225
1739
|
},
|
|
1740
|
+
async claimRequest(clientId, requestId, expiresAt) {
|
|
1741
|
+
const ttl = expiresAt - Date.now();
|
|
1742
|
+
if (ttl <= 0)
|
|
1743
|
+
return false;
|
|
1744
|
+
return unwrapResult("cache.kv.compareAndSwap", await options.cache.kv.compareAndSwap(
|
|
1745
|
+
`hai:crypto:transport:request:${clientId}:${requestId}`,
|
|
1746
|
+
void 0,
|
|
1747
|
+
true,
|
|
1748
|
+
{ px: ttl }
|
|
1749
|
+
));
|
|
1750
|
+
},
|
|
1226
1751
|
async delete(clientId) {
|
|
1227
1752
|
unwrapResult("cache.kv.del", await options.cache.kv.del(buildCacheKey(clientId)));
|
|
1228
1753
|
}
|
|
@@ -1231,20 +1756,11 @@ function createRedisTransportKeyStore(options) {
|
|
|
1231
1756
|
|
|
1232
1757
|
// src/transport/store-provider/crypto-transport-store-reldb.ts
|
|
1233
1758
|
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
|
-
}
|
|
1759
|
+
var TRANSPORT_REQUEST_TABLE = "hai_crypto_transport_requests";
|
|
1760
|
+
var TRANSPORT_CAPACITY_TABLE = "hai_crypto_transport_capacity";
|
|
1761
|
+
var PRUNE_BATCH_SIZE = 100;
|
|
1244
1762
|
function toOperationError2(operation, error) {
|
|
1245
|
-
|
|
1246
|
-
wrapped.code = error.code;
|
|
1247
|
-
return wrapped;
|
|
1763
|
+
return Object.assign(new Error(cryptoM("crypto_transportKeyStoreOperationFailed")), { operation, code: error.code });
|
|
1248
1764
|
}
|
|
1249
1765
|
function unwrapResult2(operation, result) {
|
|
1250
1766
|
if (result.success)
|
|
@@ -1252,21 +1768,66 @@ function unwrapResult2(operation, result) {
|
|
|
1252
1768
|
throw toOperationError2(operation, result.error);
|
|
1253
1769
|
}
|
|
1254
1770
|
function createReldbTransportKeyStore(options) {
|
|
1255
|
-
const ttlSeconds =
|
|
1771
|
+
const { ttlSeconds } = options;
|
|
1772
|
+
if (ttlSeconds !== void 0 && (!Number.isSafeInteger(ttlSeconds) || ttlSeconds <= 0 || ttlSeconds > 2147483647))
|
|
1773
|
+
throw new TypeError(cryptoM("crypto_transportKeyStoreTtlInvalid"));
|
|
1774
|
+
const maxClients = options.maxClients ?? 1e4;
|
|
1775
|
+
if (!Number.isSafeInteger(maxClients) || maxClients <= 0)
|
|
1776
|
+
throw new TypeError(cryptoM("crypto_transportKeyStoreCapacityInvalid"));
|
|
1256
1777
|
let ensureTablePromise = null;
|
|
1778
|
+
function nowSql() {
|
|
1779
|
+
switch (options.reldb.config?.type) {
|
|
1780
|
+
case "postgresql":
|
|
1781
|
+
return "CAST(FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000) AS BIGINT)";
|
|
1782
|
+
case "mysql":
|
|
1783
|
+
return "CAST(FLOOR(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000) AS SIGNED)";
|
|
1784
|
+
default:
|
|
1785
|
+
return "(CAST(strftime('%s', 'now') AS INTEGER) * 1000 + CAST(substr(strftime('%f', 'now'), 4, 3) AS INTEGER))";
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
function insertIfAbsent(table, columns, placeholders, key) {
|
|
1789
|
+
return options.reldb.config?.type === "mysql" ? `INSERT IGNORE INTO ${table} (${columns}) VALUES (${placeholders})` : `INSERT INTO ${table} (${columns}) VALUES (${placeholders}) ON CONFLICT (${key}) DO NOTHING`;
|
|
1790
|
+
}
|
|
1257
1791
|
async function ensureTable() {
|
|
1258
1792
|
if (!ensureTablePromise) {
|
|
1259
|
-
|
|
1793
|
+
const initialization = (async () => {
|
|
1260
1794
|
unwrapResult2(
|
|
1261
1795
|
"reldb.ddl.createTable",
|
|
1262
1796
|
await options.reldb.ddl.createTable(TRANSPORT_KEY_TABLE, {
|
|
1263
1797
|
client_id: { type: "TEXT", primaryKey: true },
|
|
1264
1798
|
public_key: { type: "TEXT", notNull: true },
|
|
1265
|
-
|
|
1266
|
-
|
|
1799
|
+
created_at_ms: { type: "BIGINT", notNull: true },
|
|
1800
|
+
expires_at_ms: { type: "BIGINT" }
|
|
1267
1801
|
}, true)
|
|
1268
1802
|
);
|
|
1803
|
+
unwrapResult2("reldb.ddl.createTable", await options.reldb.ddl.createTable(TRANSPORT_REQUEST_TABLE, {
|
|
1804
|
+
request_key: { type: "TEXT", primaryKey: true },
|
|
1805
|
+
expires_at: { type: "BIGINT", notNull: true }
|
|
1806
|
+
}, true));
|
|
1807
|
+
unwrapResult2("reldb.ddl.createTable", await options.reldb.ddl.createTable(TRANSPORT_CAPACITY_TABLE, {
|
|
1808
|
+
id: { type: "INTEGER", primaryKey: true },
|
|
1809
|
+
max_clients: { type: "BIGINT", notNull: true },
|
|
1810
|
+
revision: { type: "BIGINT", notNull: true }
|
|
1811
|
+
}, true));
|
|
1812
|
+
unwrapResult2("reldb.sql.execute", await options.reldb.sql.execute(
|
|
1813
|
+
insertIfAbsent(TRANSPORT_CAPACITY_TABLE, "id, max_clients, revision", "?, ?, ?", "id"),
|
|
1814
|
+
[1, maxClients, 0]
|
|
1815
|
+
));
|
|
1816
|
+
const capacity = unwrapResult2("reldb.sql.get", await options.reldb.sql.get(
|
|
1817
|
+
`SELECT max_clients FROM ${TRANSPORT_CAPACITY_TABLE} WHERE id = ?`,
|
|
1818
|
+
[1]
|
|
1819
|
+
));
|
|
1820
|
+
if (!capacity || Number(capacity.max_clients) !== maxClients)
|
|
1821
|
+
throw new Error(cryptoM("crypto_transportKeyStoreCapacityMismatch"));
|
|
1269
1822
|
})();
|
|
1823
|
+
ensureTablePromise = initialization;
|
|
1824
|
+
try {
|
|
1825
|
+
await initialization;
|
|
1826
|
+
} catch (error) {
|
|
1827
|
+
if (ensureTablePromise === initialization)
|
|
1828
|
+
ensureTablePromise = null;
|
|
1829
|
+
throw error;
|
|
1830
|
+
}
|
|
1270
1831
|
}
|
|
1271
1832
|
return ensureTablePromise;
|
|
1272
1833
|
}
|
|
@@ -1279,45 +1840,88 @@ function createReldbTransportKeyStore(options) {
|
|
|
1279
1840
|
)
|
|
1280
1841
|
);
|
|
1281
1842
|
}
|
|
1843
|
+
async function withCapacityLock(operation) {
|
|
1844
|
+
await ensureTable();
|
|
1845
|
+
const result = await options.reldb.tx.wrap(async (tx) => {
|
|
1846
|
+
const locked = unwrapResult2("tx.execute", await tx.execute(
|
|
1847
|
+
`UPDATE ${TRANSPORT_CAPACITY_TABLE} SET revision = revision + 1 WHERE id = ? AND max_clients = ?`,
|
|
1848
|
+
[1, maxClients]
|
|
1849
|
+
));
|
|
1850
|
+
if (locked.changes !== 1)
|
|
1851
|
+
throw new Error(cryptoM("crypto_transportKeyStoreCapacityMismatch"));
|
|
1852
|
+
return operation(tx);
|
|
1853
|
+
});
|
|
1854
|
+
return unwrapResult2("reldb.tx.wrap", result);
|
|
1855
|
+
}
|
|
1856
|
+
async function pruneRows(tx) {
|
|
1857
|
+
let removed = 0;
|
|
1858
|
+
for (const [table, id, expiry] of [[TRANSPORT_KEY_TABLE, "client_id", "expires_at_ms"], [TRANSPORT_REQUEST_TABLE, "request_key", "expires_at"]]) {
|
|
1859
|
+
removed += unwrapResult2("tx.execute", await tx.execute(
|
|
1860
|
+
`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)`
|
|
1861
|
+
)).changes;
|
|
1862
|
+
}
|
|
1863
|
+
return removed;
|
|
1864
|
+
}
|
|
1865
|
+
async function pruneExpired() {
|
|
1866
|
+
return withCapacityLock(pruneRows);
|
|
1867
|
+
}
|
|
1282
1868
|
return {
|
|
1869
|
+
get stateScope() {
|
|
1870
|
+
return options.reldb.config?.type && options.reldb.config.type !== "sqlite" ? "shared" : "local";
|
|
1871
|
+
},
|
|
1283
1872
|
async register(publicKey) {
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
"
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
)
|
|
1294
|
-
|
|
1295
|
-
|
|
1873
|
+
if (!isValidSM2PublicKey(publicKey))
|
|
1874
|
+
throw new TypeError(cryptoM("crypto_sm2PublicKeyInvalid"));
|
|
1875
|
+
const clientId = `c_${globalThis.crypto.randomUUID()}`;
|
|
1876
|
+
return withCapacityLock(async (tx) => {
|
|
1877
|
+
await pruneRows(tx);
|
|
1878
|
+
const count = unwrapResult2("tx.get", await tx.get(
|
|
1879
|
+
`SELECT COUNT(*) AS total FROM ${TRANSPORT_KEY_TABLE} WHERE expires_at_ms IS NULL OR expires_at_ms > ${nowSql()}`
|
|
1880
|
+
));
|
|
1881
|
+
const total = Number(count?.total);
|
|
1882
|
+
if (!Number.isSafeInteger(total) || total < 0)
|
|
1883
|
+
throw new Error(cryptoM("crypto_transportKeyStoreOperationFailed"));
|
|
1884
|
+
if (total >= maxClients)
|
|
1885
|
+
throw new Error(cryptoM("crypto_transportKeyStoreCapacityReached"));
|
|
1886
|
+
const inserted = unwrapResult2("tx.execute", await tx.execute(
|
|
1887
|
+
`INSERT INTO ${TRANSPORT_KEY_TABLE} (client_id, public_key, created_at_ms, expires_at_ms) VALUES (?, ?, ${nowSql()}, ${ttlSeconds === void 0 ? "NULL" : `${nowSql()} + ?`})`,
|
|
1888
|
+
ttlSeconds === void 0 ? [clientId, publicKey] : [clientId, publicKey, ttlSeconds * 1e3]
|
|
1889
|
+
));
|
|
1890
|
+
if (inserted.changes !== 1)
|
|
1891
|
+
throw new Error(cryptoM("crypto_transportClientNotRegistered"));
|
|
1892
|
+
return clientId;
|
|
1893
|
+
});
|
|
1296
1894
|
},
|
|
1297
1895
|
async get(clientId) {
|
|
1298
1896
|
await ensureTable();
|
|
1299
1897
|
const row = unwrapResult2(
|
|
1300
1898
|
"reldb.sql.get",
|
|
1301
1899
|
await options.reldb.sql.get(
|
|
1302
|
-
`SELECT public_key
|
|
1900
|
+
`SELECT public_key FROM ${TRANSPORT_KEY_TABLE} WHERE client_id = ? AND (expires_at_ms IS NULL OR expires_at_ms > ${nowSql()})`,
|
|
1303
1901
|
[clientId]
|
|
1304
1902
|
)
|
|
1305
1903
|
);
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1904
|
+
return row?.public_key;
|
|
1905
|
+
},
|
|
1906
|
+
async claimRequest(clientId, requestId, expiresAt) {
|
|
1907
|
+
await ensureTable();
|
|
1908
|
+
if (!Number.isSafeInteger(expiresAt) || expiresAt <= Date.now())
|
|
1909
|
+
return false;
|
|
1910
|
+
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`;
|
|
1911
|
+
const inserted = unwrapResult2("reldb.sql.execute", await options.reldb.sql.execute(
|
|
1912
|
+
insertSql,
|
|
1913
|
+
[JSON.stringify([clientId, requestId]), expiresAt]
|
|
1914
|
+
));
|
|
1915
|
+
return inserted.changes === 1;
|
|
1313
1916
|
},
|
|
1314
1917
|
async delete(clientId) {
|
|
1315
1918
|
await ensureTable();
|
|
1316
1919
|
await deleteClientKey(clientId);
|
|
1317
|
-
}
|
|
1920
|
+
},
|
|
1921
|
+
pruneExpired
|
|
1318
1922
|
};
|
|
1319
1923
|
}
|
|
1320
1924
|
|
|
1321
|
-
export { HaiCryptoError, TRANSPORT_PROTOCOL, createInMemoryKeyStore, createRedisTransportKeyStore, createReldbTransportKeyStore, crypto2 as crypto };
|
|
1925
|
+
export { HaiCryptoError, TRANSPORT_BODY_LIMITS, TRANSPORT_PLAINTEXT_RANGE, TRANSPORT_PROTOCOL, TransportBodyTooLargeError, TransportTimeError, authenticateTransportRequest, createInMemoryKeyStore, createRedisTransportKeyStore, createReldbTransportKeyStore, createTransportErrorResponse, crypto2 as crypto, isValidSM2PublicKey, parseTransportServerKeyRing, readTransportBody, resolveTransportBodyLimits };
|
|
1322
1926
|
//# sourceMappingURL=index.js.map
|
|
1323
1927
|
//# sourceMappingURL=index.js.map
|