@h-ai/crypto 1.0.0-beta.19 → 1.0.0-beta.20

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 CHANGED
@@ -4,28 +4,9 @@
4
4
 
5
5
  [Framework home](../../README.en.md) · Related modules: [@h-ai/cache](../cache/README.en.md) · [@h-ai/core](../core/README.en.md) · [@h-ai/reldb](../reldb/README.en.md)
6
6
 
7
- ## Introduction
8
-
9
7
  A cryptography module for asymmetric encryption, hashing, symmetric encryption, and password hashing.
10
8
 
11
- ## Core features
12
-
13
- - Asymmetric cryptography (key generation, encryption/decryption, signing/verification)
14
- - Hashing (hash, HMAC, verification)
15
- - Symmetric encryption (ECB/CBC modes)
16
- - Password hashing (salted iterative hashes)
17
- - Transport encryption (`crypto.transport`: server manager + client `encryptedFetch`)
18
- - Shared server/browser support (Node.js / browser)
19
-
20
- ## Getting started
21
-
22
- ### Security notice
23
-
24
- - **SM4 encryption returns a structured payload by default**: `crypto.symmetric.encrypt(data, key)` uses CBC + a random IV by default and returns `{ mode, ciphertext, iv, encoding }`. Pass that object to `crypto.symmetric.decrypt(payload, key)` to decrypt.
25
- - **Use random SM4 keys**: generate keys with `crypto.symmetric.generateKey()`. This module does not provide a KDF for password-based encryption keys. Store passwords with the server-only `crypto.password.hash(password)`; do not use passwords or password hashes directly as SM4 keys.
26
- - **IVs must be unique**: never reuse an IV with the same key. Prefer default CBC or `encryptWithIV()` to generate one for each call. Transmit and store the IV with the ciphertext; the IV can be public.
27
-
28
- ### Quick start
9
+ ## Quick Start
29
10
 
30
11
  Concurrent `close()` invalidates an unfinished `init()`, so late initialization cannot reopen the module. Wait for that initialization to settle before initializing again.
31
12
 
@@ -38,7 +19,55 @@ if (!initialized.success)
38
19
  throw new Error(initialized.error.message)
39
20
  ```
40
21
 
41
- #### Asymmetric cryptography (encryption / signing)
22
+ ## Features
23
+
24
+ ### Purpose, inputs and outputs
25
+
26
+ Initialize crypto successfully before using algorithms. Check HaiResult.success before reading data of type T; void has no business data and Promises must be awaited. Keys, ciphertext, signatures, digests and payloads come from the matching successful operations. manager is createServer.data after success; session is returned directly by createClient. HTTP adapters derive requestContext/messageContext from the actual request, not a client-supplied identity. Complete workflows follow below.
27
+
28
+ | Entry | Use case | Input (`—` means none) | Return value | Call example |
29
+ | ------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
30
+ | `crypto.init` | Prepare cryptographic operations | `—` | `Promise<HaiResult<void>>` | `await crypto.init()` |
31
+ | `crypto.close` | Release module state | `—` | `Promise<void>` | `await crypto.close()` |
32
+ | `crypto.isInitialized` | Check readiness | `—` | `boolean` | `crypto.isInitialized` |
33
+ | `crypto.asymmetric.generateKeyPair` | Generate an SM2 key pair | `—` | `HaiResult<{ publicKey, privateKey }>` | `crypto.asymmetric.generateKeyPair()` |
34
+ | `crypto.asymmetric.encrypt` | Encrypt with the recipient public key | `data: string, publicKey: string, options?: { cipherMode, outputFormat }` | `HaiResult<string>` | `crypto.asymmetric.encrypt("orders", publicKey)` |
35
+ | `crypto.asymmetric.decrypt` | Decrypt with the private key | `ciphertext: string, privateKey: string, options?` | `HaiResult<string>` | `crypto.asymmetric.decrypt(ciphertext, privateKey)` |
36
+ | `crypto.asymmetric.sign` | Sign a message | `data: string, privateKey: string, options?: { hash, userId }` | `HaiResult<string>` | `crypto.asymmetric.sign("orders", privateKey)` |
37
+ | `crypto.asymmetric.verify` | Verify a message signature | `data: string, signature: string, publicKey: string, options?` | `HaiResult<boolean>` | `crypto.asymmetric.verify("orders", signature, publicKey)` |
38
+ | `crypto.asymmetric.isValidPublicKey` | Check public-key format | `publicKey: string` | `boolean` | `crypto.asymmetric.isValidPublicKey(publicKey)` |
39
+ | `crypto.asymmetric.isValidPrivateKey` | Check private-key format | `privateKey: string` | `boolean` | `crypto.asymmetric.isValidPrivateKey(privateKey)` |
40
+ | `crypto.hash.hash` | Compute an SM3 digest | `data: string / Uint8Array, options?: { inputEncoding }` | `HaiResult<string>: 64 hex` | `crypto.hash.hash("orders")` |
41
+ | `crypto.hash.hmac` | Compute a keyed digest | `data: string, key: string` | `HaiResult<string>` | `crypto.hash.hmac("orders", hmacKey)` |
42
+ | `crypto.hash.verify` | Compare text with a digest | `data: string, expectedHash: string` | `HaiResult<boolean>` | `crypto.hash.verify("orders", digest)` |
43
+ | `crypto.symmetric.generateKey` | Generate a random SM4 key | `—` | `string: 32 hex` | `crypto.symmetric.generateKey()` |
44
+ | `crypto.symmetric.generateIV` | Generate a random IV | `—` | `string: 32 hex` | `crypto.symmetric.generateIV()` |
45
+ | `crypto.symmetric.encrypt` | Encrypt with structured metadata | `data: string, key: 32 hex, options?: { mode, iv, outputFormat }` | `HaiResult<{ mode, ciphertext, iv?, encoding }>` | `crypto.symmetric.encrypt("orders", key)` |
46
+ | `crypto.symmetric.decrypt` | Decrypt a structured payload | `payload: SymmetricEncryptedPayload, key: 32 hex` | `HaiResult<string>` | `crypto.symmetric.decrypt(payload, key)` |
47
+ | `crypto.symmetric.encryptWithIV` | Encrypt with a fresh IV | `data: string, key: 32 hex` | `HaiResult<{ mode: "cbc", ciphertext, iv, encoding }>` | `crypto.symmetric.encryptWithIV("orders", key)` |
48
+ | `crypto.symmetric.decryptWithIV` | Decrypt ciphertext with its IV | `ciphertext: string, key: 32 hex, iv: 32 hex` | `HaiResult<string>` | `crypto.symmetric.decryptWithIV(ciphertext, key, iv)` |
49
+ | `crypto.symmetric.isValidKey` | Check key format | `key: string` | `boolean` | `crypto.symmetric.isValidKey(key)` |
50
+ | `crypto.symmetric.isValidIV` | Check IV format | `iv: string` | `boolean` | `crypto.symmetric.isValidIV(iv)` |
51
+ | `crypto.password.hash` | Hash a password on the server | `password: string, options?: { saltLength, iterations }` | `HaiResult<string>: $hai$…` | `crypto.password.hash(password)` |
52
+ | `crypto.password.verify` | Verify a password on the server | `password: string, storedHash: string` | `HaiResult<boolean>` | `crypto.password.verify(password, storedHash)` |
53
+ | `crypto.transport.createServer` | Create a shared transport manager | `{ serverKeyRing, keyStore, maxPlaintextBytes?, … }` | `HaiResult<TransportEncryptionManager>` | `crypto.transport.createServer({ serverKeyRing, keyStore })` |
54
+ | `crypto.transport.createClient` | Create an encrypted HTTP session | `{ keyExchangeUrl: HTTPS URL, fetch?, maxPlaintextBytes? }` | `TransportClient: init / encryptedFetch / destroy` | `crypto.transport.createClient({ keyExchangeUrl: "https://api.example.com/api/v1/_hai/key-exchange" })` |
55
+ | `crypto.transport.protocol` | Read protocol paths and headers | `—` | `TRANSPORT_PROTOCOL_T` | `crypto.transport.protocol` |
56
+ | `manager.getServerKeyId` | Read the active server key ID | `—` | `string` | `manager.getServerKeyId()` |
57
+ | `manager.getServerPublicKey` | Read the handshake public key | `—` | `string` | `manager.getServerPublicKey()` |
58
+ | `manager.registerClientKey` | Register a client public key | `clientPublicKey: string` | `Promise<string>: clientId` | `await manager.registerClientKey(clientPublicKey)` |
59
+ | `manager.getClientPublicKey` | Read a registered client key | `clientId: string` | `Promise<string / undefined>` | `await manager.getClientPublicKey(clientId)` |
60
+ | `manager.decryptRequest` | Authenticate and decrypt a request | `payload: EncryptedRequestPayload, expected: TransportRequestContext` | `Promise<HaiResult<string>>` | `await manager.decryptRequest(encryptedRequest, requestContext)` |
61
+ | `manager.encryptResponse` | Authenticate and encrypt a response | `request: TransportMessageContext, data: string, status: number` | `Promise<HaiResult<EncryptedPayload>>` | `await manager.encryptResponse(messageContext, "{}", 200)` |
62
+ | `manager.close` | Release manager resources | `—` | `Promise<void>` | `await manager.close()` |
63
+ | `session.init` | Initialize the encrypted session | `—` | `Promise<HaiResult<void>>` | `await session.init()` |
64
+ | `session.encryptedFetch` | Call an encrypted endpoint | `input: RequestInfo / URL, init?: RequestInit` | `Promise<Response>` | `await session.encryptedFetch("https://api.example.com/api/v1/orders")` |
65
+ | `session.ready` | Check handshake readiness | `—` | `boolean` | `session.ready()` |
66
+ | `session.destroy` | Clear the local transport session | `—` | `void` | `session.destroy()` |
67
+
68
+ Shared server/browser support (Node.js / browser)
69
+
70
+ ### Asymmetric cryptography (encryption / signing)
42
71
 
43
72
  ```ts
44
73
  // 生成密钥对
@@ -69,7 +98,7 @@ crypto.asymmetric.isValidPublicKey(publicKey) // true
69
98
  crypto.asymmetric.isValidPrivateKey(privateKey) // true
70
99
  ```
71
100
 
72
- #### Hashing (Hash / HMAC / verification)
101
+ ### Hashing (Hash / HMAC / verification)
73
102
 
74
103
  ```ts
75
104
  // 字符串哈希
@@ -93,7 +122,7 @@ if (hash.success) {
93
122
  }
94
123
  ```
95
124
 
96
- #### Symmetric encryption (default CBC / structured payload)
125
+ ### Symmetric encryption (default CBC / structured payload)
97
126
 
98
127
  ```ts
99
128
  // 生成随机密钥和 IV
@@ -134,7 +163,7 @@ crypto.symmetric.isValidKey(key) // true
134
163
  crypto.symmetric.isValidIV(iv) // true
135
164
  ```
136
165
 
137
- #### Password hashing (salted iterations)
166
+ ### Password hashing (salted iterations)
138
167
 
139
168
  ```ts
140
169
  // 哈希密码(输出格式: $hai$<iterations>$<salt>$<hash>)
@@ -156,7 +185,7 @@ if (hashed.success) {
156
185
  }
157
186
  ```
158
187
 
159
- #### Transport encryption (crypto.transport)
188
+ ### Transport encryption (crypto.transport)
160
189
 
161
190
  Transport client `destroy()` invalidates and aborts an in-flight handshake; late results cannot restore keys or overwrite the next handshake. It clears local state; server registry entries retain their own expiry.
162
191
 
@@ -171,7 +200,7 @@ For typical applications, use the higher-level integrations:
171
200
 
172
201
  Call `crypto.transport.createServer({ serverKeyRing, keyStore })` / `createClient()` directly only for custom runtimes, tests, or manually integrated HTTP key exchange endpoints.
173
202
 
174
- ##### Workflow
203
+ #### Workflow
175
204
 
176
205
  1. Call `await crypto.init()` to initialize `crypto.asymmetric` and `crypto.symmetric`.
177
206
  2. On the server, create a `manager` with `crypto.transport.createServer({ serverKeyRing, keyStore })`. It holds the server key pair and stores client public keys. Inject a shared `keyStore` for multi-node deployments.
@@ -180,7 +209,7 @@ Call `crypto.transport.createServer({ serverKeyRing, keyStore })` / `createClien
180
209
  5. Every request sends an authenticated envelope `{ context, direction, status, encryptedKey, ciphertext, iv, tag, signature }`, including empty POST and GET/HEAD.
181
210
  6. Custom adapters first await `authenticateTransportRequest(manager, request)`. Only successful `data.plaintext` reaches business code; retain `data.context`.
182
211
  7. Encrypt responses with `await manager.encryptResponse(context, data, response.status)`; clients verify context, status and authenticity before returning plaintext.
183
- 8. Clear a client session with `client.destroy()`. Release server resources with `manager.close()` or module-level `await crypto.close()`.
212
+ 8. Clear the local client session with `client.destroy()`. On the server, first `await manager.close()`, then `await crypto.close()`. Closing the module does not close independently created managers or revoke application sessions.
184
213
 
185
214
  > Servers must explicitly inject the same managed `serverKeyRing` and a shared `keyStore` (Redis or a shared database). Sharing client public keys alone cannot enable cross-node decryption. There is no implicit process key or memory-registry fallback; development/tests may explicitly use `createInMemoryKeyStore(capacity)`. Reload the same ring after restart; sticky sessions are unnecessary.
186
215
 
@@ -194,7 +223,7 @@ Registration validates the encoding and SM2 curve point before writing. Only 128
194
223
 
195
224
  Omitting Redis/DB `ttlSeconds` keeps keys indefinitely. Explicit values must be integer seconds from 1 to 2147483647; a finite TTL is recommended. Redis uses native per-key expiration and does not expose the DB `maxClients` limit; configure memory budgets without evicting valid nonces. DB `maxClients` is a positive safe integer, defaults to 10000, and must match the persistent setting on every node. Registration locks a shared capacity row inside its transaction before counting and inserting; a full registry rejects registration without evicting valid keys or nonces. Deleting a client does not delete its valid nonces.
196
225
 
197
- DB expiry uses database epoch milliseconds. Synchronize both database and node clocks within the protocol limits. Failed initial table creation can be retried. Each registration prunes one batch; `await registry.pruneExpired()` deletes at most 100 expired keys and 100 expired nonces per call. Schedule it in the application for low-traffic deployments: the registry creates no background timers and does not close the injected reldb. Migrate old timestamp key columns offline to BIGINT `created_at_ms` / `expires_at_ms`. Capacity is persisted in `hai_crypto_transport_capacity`; coordinate changes across all nodes and that row, and never clear valid nonces to recreate a registry.
226
+ DB expiry uses database epoch milliseconds. Synchronize both database and node clocks within the protocol limits. Failed initial table creation can be retried. Each registration prunes one batch; `await registry.pruneExpired()` deletes at most 100 expired keys and 100 expired nonces per call. Schedule it in the application for low-traffic deployments: the registry creates no background timers and does not close the injected reldb. Key expiry uses BIGINT columns `created_at_ms` / `expires_at_ms`. Capacity is persisted in `hai_crypto_transport_capacity`; coordinate changes across all nodes and that row, and never clear valid nonces to recreate a registry.
198
227
 
199
228
  ```ts
200
229
  const registry = createReldbTransportKeyStore({ reldb, ttlSeconds: 3600, maxClients: 10000 })
@@ -256,7 +285,7 @@ Only authenticated protocol v2 is accepted. Independent random SM4-CBC and HMAC-
256
285
 
257
286
  Empty POSTs still send an authenticated envelope for empty plaintext. GET/HEAD carry it in X-Hai-Transport; bodyless responses such as 204/304/HEAD use the same header. Other requests/responses carry a JSON envelope and X-Encrypted: true. The client must match the original request context and HTTP status; missing markers, old formats and authentication failures reject. Serv/Kit hosts explicitly allow plaintext through `encryption: disabled` path rules or host `excludePaths`. Callers must select ordinary requests; API Client/Kit wrappers use their own `excludePaths`. CORS must allow and expose X-Hai-Transport. TLS authenticates handshake keys; this protocol does not replace IAM authorization.
258
287
 
259
- Set `transport.maxPlaintextBytes` for both request and response JSON plaintext, measured in UTF-8 bytes: default 1 MiB, integer range 1 KiB–64 MiB. Clients learn the server limit during key exchange. `resolveTransportBodyLimits()` derives encrypted/proxy bytes as twice the plaintext limit plus 32 KiB. Handshake, public-key, header and replay constraints remain independent. Oversized streams are cancelled and readers released: requests return 413; oversized responses return an encrypted 500 capacity error, recognized as `TransportBodyTooLargeError` / `hai:crypto:070`. [容量配置、文件换算与升级](TRANSPORT-CAPACITY.md)。
288
+ Set `transport.maxPlaintextBytes` for both request and response JSON plaintext, measured in UTF-8 bytes: default 1 MiB, integer range 1 KiB–64 MiB. Clients learn the server limit during key exchange. `resolveTransportBodyLimits()` derives encrypted/proxy bytes as twice the plaintext limit plus 32 KiB. Handshake, public-key, header and replay constraints remain independent. Oversized streams are cancelled and readers released: requests return 413; oversized responses return an encrypted 500 capacity error, recognized as `TransportBodyTooLargeError` / `hai:crypto:070`. [Capacity and file-size calculation](TRANSPORT-CAPACITY.md)。
260
289
 
261
290
  Ordinary routes retain `transport.maxPlaintextBytes` (auto/omitted: 1 MiB). Declare Feedback report intake, evidence, attachments and ZIP download capacity separately using `routePlaintextBytes` and `feedback.budget.maxTransportBytes`; `*` matches one complete path segment and explicit host route values take precedence. Handshakes, adapter-node and proxies use the maximum route capacity. HTTP adapters admit at most 2 explicitly declared material requests above 1 MiB and a separate 16 ordinary business requests, releasing admission after the entire response or failure. Busy admission returns 503 / `hai:crypto:072`; business writes are not automatically replayed.
262
291
 
@@ -276,33 +305,32 @@ const text = await readTransportBody(request.body, {
276
305
  const payload: unknown = JSON.parse(text)
277
306
  ```
278
307
 
279
- #### Close the module
308
+ ### Close the module
280
309
 
281
310
  ```ts
282
311
  // 使用完毕后关闭,释放内部状态
283
312
  await crypto.close()
284
313
  ```
285
314
 
286
- ### Error handling
287
-
288
- All operations return `HaiResult<T>`:
315
+ ### Security notice
289
316
 
290
- ```ts
291
- const result = crypto.asymmetric.encrypt('data', publicKey)
292
- if (!result.success) {
293
- // result.error.code / result.error.message
294
- }
295
- ```
317
+ - **SM4 encryption returns a structured payload by default**: `crypto.symmetric.encrypt(data, key)` uses CBC + a random IV by default and returns `{ mode, ciphertext, iv, encoding }`. Pass that object to `crypto.symmetric.decrypt(payload, key)` to decrypt.
318
+ - **Use random SM4 keys**: generate keys with `crypto.symmetric.generateKey()`. This module does not provide a KDF for password-based encryption keys. Store passwords with the server-only `crypto.password.hash(password)`; do not use passwords or password hashes directly as SM4 keys.
319
+ - **IVs must be unique**: never reuse an IV with the same key. Prefer default CBC or `encryptWithIV()` to generate one for each call. Transmit and store the IV with the ciphertext; the IV can be public.
296
320
 
297
321
  ### Optional integrations
298
322
 
299
323
  The admin console's online encryption demo calls the Crypto SM3/SM4 APIs directly. SM4 uses a random temporary key and IV and verifies decryption before showing success; keys are not saved. Changing input clears previous results. Random digests or Base64 must not be presented as cryptographic results.
300
324
 
301
- ## Production shared state
325
+ ## Configuration
326
+
327
+ crypto.init() takes no configuration object. Algorithm options belong to individual calls; configure HTTP transport with crypto.transport.createServer/createClient options and explicitly inject the server key ring and shared registry. Keep private keys out of browsers and public YAML.
328
+
329
+ ### Production shared state
302
330
 
303
331
  Production transport registries must declare stateScope=shared and actually use Redis or PostgreSQL/MySQL. Memory, Redis factories over memory, and RelDB factories over SQLite are rejected. All nodes load the same managed versioned key ring. Initialization checks apply when NODE_ENV or HAI_CORE_ENV is production; only explicit HAI_E2E=1/HAI_E2E_MOCK=1 tests are exempt. [State, concurrency and recovery (Chinese)](../../.agents/instructions/distributed-state.md)。
304
332
 
305
- ## Module requirements and explicit limits
333
+ ### Module requirements and explicit limits
306
334
 
307
335
  `resolveTransportBodyLimits(limit?, { requiredPlaintextBytes? })` derives capacity from enabled modules only when limit is omitted; without module requirements it remains 1 MiB. A smaller explicit limit throws RangeError instead of silently increasing or reducing capacity. All values remain bounded to 1 KiB–64 MiB; encrypted capacity is twice plaintext plus 32 KiB. Serv/Kit configuration accepts `auto`, normalized to undefined at runtime.
308
336
 
@@ -313,7 +341,18 @@ const limits = resolveTransportBodyLimits(24 * 1024 * 1024, { requiredPlaintextB
313
341
  // plaintextBytes: 25165824; encryptedPayloadBytes: 50364416
314
342
  ```
315
343
 
316
- ## Development and testing
344
+ ## Error Handling
345
+
346
+ Encryption, signatures, hashes and password operations return `HaiResult<T>`; check `success` first. Random key/IV and format validators return direct values, and `crypto.close()` returns `Promise<void>`. `encryptedFetch` rejects like fetch; catch errors from stream readers and explicitly throwing capacity helpers at their call boundary:
347
+
348
+ ```ts
349
+ const result = crypto.asymmetric.encrypt('data', publicKey)
350
+ if (!result.success) {
351
+ // result.error.code / result.error.message
352
+ }
353
+ ```
354
+
355
+ ## Development
317
356
 
318
357
  ```bash
319
358
  pnpm --filter @h-ai/crypto test
package/README.ja.md CHANGED
@@ -4,30 +4,9 @@
4
4
 
5
5
  [フレームワークのホーム](../../README.ja.md) · 関連モジュール:[@h-ai/cache](../cache/README.ja.md) · [@h-ai/core](../core/README.ja.md) · [@h-ai/reldb](../reldb/README.ja.md)
6
6
 
7
- ## 概要
8
-
9
7
  非対称暗号、ハッシュ、対称暗号、パスワードハッシュを提供する暗号モジュールです。
10
8
 
11
- ## 主な機能
12
-
13
- - 非対称暗号(鍵生成、暗号化・復号、署名・検証)
14
- - ハッシュ(Hash、HMAC、検証)
15
- - 対称暗号(ECB/CBC モード)
16
- - パスワードハッシュ(ソルト付き反復ハッシュ)
17
- - 通信暗号化(`crypto.transport`:サーバー管理機能 + クライアントの `encryptedFetch`)
18
- - サーバー・ブラウザー共通(Node.js / ブラウザー)
19
-
20
- ## 使い始める
21
-
22
- ### セキュリティに関する注意
23
-
24
- 伝送クライアントの `destroy()` は進行中のハンドシェイクを無効化して中断し、遅延結果による鍵の復活や次回ハンドシェイクの上書きを防ぎます。サーバー registry は自身の期限で回収します。
25
-
26
- - **SM4 の既定の暗号化結果は構造化された暗号文**:`crypto.symmetric.encrypt(data, key)` は既定で CBC + ランダム IV を使用し、`{ mode, ciphertext, iv, encoding }` を返します。復号時はこのオブジェクトを `crypto.symmetric.decrypt(payload, key)` に渡します。
27
- - **SM4 にはランダムな鍵を使用してください**:`crypto.symmetric.generateKey()` で鍵を生成します。このモジュールはパスワードから暗号鍵を導出する KDF を提供しません。パスワードの保存にはサーバー専用の `crypto.password.hash(password)` を使用し、パスワードやそのハッシュを直接 SM4 の鍵にしないでください。
28
- - **IV は毎回一意にしてください**:同じ鍵で IV を再利用してはいけません。既定の CBC または `encryptWithIV()` による毎回の自動生成を推奨します。IV は暗号文と一緒に送信・保存します(公開可能で、秘密にする必要はありません)。
29
-
30
- ### クイックスタート
9
+ ## クイックスタート
31
10
 
32
11
  並行する `close()` は未完了の `init()` を無効にし、遅い初期化がモジュールを再開しないようにします。その初期化の完了を待ってから再初期化できます。
33
12
 
@@ -40,7 +19,55 @@ if (!initialized.success)
40
19
  throw new Error(initialized.error.message)
41
20
  ```
42
21
 
43
- #### 非対称暗号(暗号化・復号 / 署名・検証)
22
+ ## 機能
23
+
24
+ ### 用途・入力・出力
25
+
26
+ アルゴリズムを使う前に crypto の初期化成功を確認します。HaiResult<T> は success の後に T 型の data を読み、void は業務データなし、Promise は await します。鍵・暗号文・署名・ダイジェストは対応する成功結果から取得します。manager は createServer 成功時の data、session は createClient の直接の戻り値です。requestContext/messageContext は HTTP アダプターが実際の要求から構築し、クライアントが申告した身元を信用しません。完全な流れは下で説明します。
27
+
28
+ | 入口 | 使用場面 | 入力(`—` は引数なし) | 戻り値 | 呼出例 |
29
+ | ------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
30
+ | `crypto.init` | 暗号化機能を準備する | `—` | `Promise<HaiResult<void>>` | `await crypto.init()` |
31
+ | `crypto.close` | モジュールの状態を解放する | `—` | `Promise<void>` | `await crypto.close()` |
32
+ | `crypto.isInitialized` | 初期化状態を確認する | `—` | `boolean` | `crypto.isInitialized` |
33
+ | `crypto.asymmetric.generateKeyPair` | SM2 鍵ペアを生成する | `—` | `HaiResult<{ publicKey, privateKey }>` | `crypto.asymmetric.generateKeyPair()` |
34
+ | `crypto.asymmetric.encrypt` | 相手の公開鍵で暗号化する | `data: string, publicKey: string, options?: { cipherMode, outputFormat }` | `HaiResult<string>` | `crypto.asymmetric.encrypt("orders", publicKey)` |
35
+ | `crypto.asymmetric.decrypt` | 秘密鍵で復号する | `ciphertext: string, privateKey: string, options?` | `HaiResult<string>` | `crypto.asymmetric.decrypt(ciphertext, privateKey)` |
36
+ | `crypto.asymmetric.sign` | メッセージに署名する | `data: string, privateKey: string, options?: { hash, userId }` | `HaiResult<string>` | `crypto.asymmetric.sign("orders", privateKey)` |
37
+ | `crypto.asymmetric.verify` | 署名を検証する | `data: string, signature: string, publicKey: string, options?` | `HaiResult<boolean>` | `crypto.asymmetric.verify("orders", signature, publicKey)` |
38
+ | `crypto.asymmetric.isValidPublicKey` | 公開鍵の形式を確認する | `publicKey: string` | `boolean` | `crypto.asymmetric.isValidPublicKey(publicKey)` |
39
+ | `crypto.asymmetric.isValidPrivateKey` | 秘密鍵の形式を確認する | `privateKey: string` | `boolean` | `crypto.asymmetric.isValidPrivateKey(privateKey)` |
40
+ | `crypto.hash.hash` | SM3 ダイジェストを計算する | `data: string / Uint8Array, options?: { inputEncoding }` | `HaiResult<string>: 64 hex` | `crypto.hash.hash("orders")` |
41
+ | `crypto.hash.hmac` | 鍵付きダイジェストを計算する | `data: string, key: string` | `HaiResult<string>` | `crypto.hash.hmac("orders", hmacKey)` |
42
+ | `crypto.hash.verify` | テキストとダイジェストを照合する | `data: string, expectedHash: string` | `HaiResult<boolean>` | `crypto.hash.verify("orders", digest)` |
43
+ | `crypto.symmetric.generateKey` | ランダムな SM4 鍵を生成する | `—` | `string: 32 hex` | `crypto.symmetric.generateKey()` |
44
+ | `crypto.symmetric.generateIV` | ランダムな IV を生成する | `—` | `string: 32 hex` | `crypto.symmetric.generateIV()` |
45
+ | `crypto.symmetric.encrypt` | 復号用の情報とともに暗号化する | `data: string, key: 32 hex, options?: { mode, iv, outputFormat }` | `HaiResult<{ mode, ciphertext, iv?, encoding }>` | `crypto.symmetric.encrypt("orders", key)` |
46
+ | `crypto.symmetric.decrypt` | 構造化した暗号文を復号する | `payload: SymmetricEncryptedPayload, key: 32 hex` | `HaiResult<string>` | `crypto.symmetric.decrypt(payload, key)` |
47
+ | `crypto.symmetric.encryptWithIV` | 新しい IV で暗号化する | `data: string, key: 32 hex` | `HaiResult<{ mode: "cbc", ciphertext, iv, encoding }>` | `crypto.symmetric.encryptWithIV("orders", key)` |
48
+ | `crypto.symmetric.decryptWithIV` | 指定した IV で復号する | `ciphertext: string, key: 32 hex, iv: 32 hex` | `HaiResult<string>` | `crypto.symmetric.decryptWithIV(ciphertext, key, iv)` |
49
+ | `crypto.symmetric.isValidKey` | 鍵の形式を確認する | `key: string` | `boolean` | `crypto.symmetric.isValidKey(key)` |
50
+ | `crypto.symmetric.isValidIV` | IV の形式を確認する | `iv: string` | `boolean` | `crypto.symmetric.isValidIV(iv)` |
51
+ | `crypto.password.hash` | サーバーでパスワードをハッシュ化する | `password: string, options?: { saltLength, iterations }` | `HaiResult<string>: $hai$…` | `crypto.password.hash(password)` |
52
+ | `crypto.password.verify` | サーバーでパスワードを検証する | `password: string, storedHash: string` | `HaiResult<boolean>` | `crypto.password.verify(password, storedHash)` |
53
+ | `crypto.transport.createServer` | 共有トランスポート管理器を作成する | `{ serverKeyRing, keyStore, maxPlaintextBytes?, … }` | `HaiResult<TransportEncryptionManager>` | `crypto.transport.createServer({ serverKeyRing, keyStore })` |
54
+ | `crypto.transport.createClient` | 暗号化 HTTP セッションを作成する | `{ keyExchangeUrl: HTTPS URL, fetch?, maxPlaintextBytes? }` | `TransportClient: init / encryptedFetch / destroy` | `crypto.transport.createClient({ keyExchangeUrl: "https://api.example.com/api/v1/_hai/key-exchange" })` |
55
+ | `crypto.transport.protocol` | プロトコルのパスとヘッダー名を参照する | `—` | `TRANSPORT_PROTOCOL_T` | `crypto.transport.protocol` |
56
+ | `manager.getServerKeyId` | 有効なサーバー鍵 ID を読む | `—` | `string` | `manager.getServerKeyId()` |
57
+ | `manager.getServerPublicKey` | ハンドシェイクの公開鍵を読む | `—` | `string` | `manager.getServerPublicKey()` |
58
+ | `manager.registerClientKey` | クライアントの公開鍵を登録する | `clientPublicKey: string` | `Promise<string>: clientId` | `await manager.registerClientKey(clientPublicKey)` |
59
+ | `manager.getClientPublicKey` | 登録済みの公開鍵を読む | `clientId: string` | `Promise<string / undefined>` | `await manager.getClientPublicKey(clientId)` |
60
+ | `manager.decryptRequest` | リクエストを認証して復号する | `payload: EncryptedRequestPayload, expected: TransportRequestContext` | `Promise<HaiResult<string>>` | `await manager.decryptRequest(encryptedRequest, requestContext)` |
61
+ | `manager.encryptResponse` | 対応する応答を認証して暗号化する | `request: TransportMessageContext, data: string, status: number` | `Promise<HaiResult<EncryptedPayload>>` | `await manager.encryptResponse(messageContext, "{}", 200)` |
62
+ | `manager.close` | 管理器のリソースを解放する | `—` | `Promise<void>` | `await manager.close()` |
63
+ | `session.init` | 暗号化セッションを初期化する | `—` | `Promise<HaiResult<void>>` | `await session.init()` |
64
+ | `session.encryptedFetch` | 暗号化エンドポイントを呼び出す | `input: RequestInfo / URL, init?: RequestInit` | `Promise<Response>` | `await session.encryptedFetch("https://api.example.com/api/v1/orders")` |
65
+ | `session.ready` | ハンドシェイク済みか確認する | `—` | `boolean` | `session.ready()` |
66
+ | `session.destroy` | ローカルの暗号化セッションを消去する | `—` | `void` | `session.destroy()` |
67
+
68
+ サーバー・ブラウザー共通(Node.js / ブラウザー)
69
+
70
+ ### 非対称暗号(暗号化・復号 / 署名・検証)
44
71
 
45
72
  ```ts
46
73
  // 生成密钥对
@@ -71,7 +98,7 @@ crypto.asymmetric.isValidPublicKey(publicKey) // true
71
98
  crypto.asymmetric.isValidPrivateKey(privateKey) // true
72
99
  ```
73
100
 
74
- #### ハッシュ(Hash / HMAC / 検証)
101
+ ### ハッシュ(Hash / HMAC / 検証)
75
102
 
76
103
  ```ts
77
104
  // 字符串哈希
@@ -95,7 +122,7 @@ if (hash.success) {
95
122
  }
96
123
  ```
97
124
 
98
- #### 対称暗号(既定の CBC / 構造化暗号文)
125
+ ### 対称暗号(既定の CBC / 構造化暗号文)
99
126
 
100
127
  ```ts
101
128
  // 生成随机密钥和 IV
@@ -136,7 +163,7 @@ crypto.symmetric.isValidKey(key) // true
136
163
  crypto.symmetric.isValidIV(iv) // true
137
164
  ```
138
165
 
139
- #### パスワードハッシュ(ソルト付き反復)
166
+ ### パスワードハッシュ(ソルト付き反復)
140
167
 
141
168
  ```ts
142
169
  // 哈希密码(输出格式: $hai$<iterations>$<salt>$<hash>)
@@ -158,7 +185,7 @@ if (hashed.success) {
158
185
  }
159
186
  ```
160
187
 
161
- #### 通信暗号化(crypto.transport)
188
+ ### 通信暗号化(crypto.transport)
162
189
 
163
190
  正規化、検証順序、nonce 保存は [Transport v2 プロトコル](TRANSPORT-PROTOCOL.md) を参照してください。
164
191
 
@@ -171,7 +198,7 @@ if (hashed.success) {
171
198
 
172
199
  `crypto.transport.createServer({ serverKeyRing, keyStore })` / `createClient()` の直接呼び出しは、独自ランタイム、テスト、HTTP 鍵交換 endpoint を自分で接続する場合に限って推奨します。
173
200
 
174
- ##### 使用手順
201
+ #### 使用手順
175
202
 
176
203
  1. `await crypto.init()` を呼び、`crypto.asymmetric` と `crypto.symmetric` を初期化します。
177
204
  2. サーバーで `crypto.transport.createServer({ serverKeyRing, keyStore })` を呼び、`manager` を作成します。サーバー鍵ペアとクライアント公開鍵を管理します。複数ノードでは共有 `keyStore` を注入できます。
@@ -180,7 +207,7 @@ if (hashed.success) {
180
207
  5. 空の POST と GET/HEAD を含め、すべての要求が認証封装 `{ context, direction, status, encryptedKey, ciphertext, iv, tag, signature }` を送ります。
181
208
  6. 独自アダプターは先に `await authenticateTransportRequest(manager, request)` を実行し、成功時のみ `data.plaintext` を業務へ渡し、`data.context` を保持します。
182
209
  7. 応答は `await manager.encryptResponse(context, data, response.status)` で元要求に関連付け、クライアントがコンテキスト・status・完全性を検証してから平文を返します。
183
- 8. `client.destroy()` でクライアントセッションを消去します。サーバー側のリソースは `manager.close()` またはモジュール単位の `await crypto.close()` で解放します。
210
+ 8. `client.destroy()` でローカルのセッションを消去します。サーバーでは `await manager.close()` の後に `await crypto.close()` を呼びます。モジュールを閉じても、独立して作成した管理器の終了や業務セッションの失効は行いません。
184
211
 
185
212
  > サーバーには同じ管理済み `serverKeyRing` と共有 `keyStore`(Redis または共有 DB)を明示的に渡します。クライアント公開鍵の共有だけではノード間で復号できません。暗黙のプロセス鍵・メモリー registry はなく、開発・テストでは `createInMemoryKeyStore(capacity)` を明示します。再起動後は同じ鍵環を読み込み、sticky session は不要です。
186
213
 
@@ -194,7 +221,7 @@ if (hashed.success) {
194
221
 
195
222
  Redis/DB の `ttlSeconds` を省略すると無期限です。指定時は 1–2147483647 の整数秒とし、有限 TTL を推奨します。Redis はネイティブのキー TTL を使い、DB の `maxClients` 上限は提供しません。メモリー予算を設定し、有効な nonce を追い出さないでください。DB の `maxClients` は正の安全な整数、既定 10000 で、全ノードと永続容量レコードが一致する必要があります。登録はトランザクション内で共有容量行をロックし、件数確認後に挿入します。満杯では登録を拒否し、有効な公開鍵や nonce を削除しません。クライアント削除時も有効な nonce は保持します。
196
223
 
197
- DB の期限は DB 時計の epoch ミリ秒で生成・判定します。DB と各ノードの時計をプロトコルの許容範囲内に同期してください。初回テーブル作成失敗後は再試行できます。登録時に 1 バッチ清掃し、`await registry.pruneExpired()` は期限切れ公開鍵と nonce を各 100 件まで削除します。低トラフィック時もアプリで定期実行してください。暗黙のタイマーは作らず、注入された reldb も閉じません。旧 timestamp 列は停止中に BIGINT の `created_at_ms` / `expires_at_ms` に移行します。容量は `hai_crypto_transport_capacity` に永続化されるため、変更時は全ノードと保存行を調整し、registry 再作成のために有効な nonce を消さないでください。
224
+ DB の期限は DB 時計の epoch ミリ秒で生成・判定します。DB と各ノードの時計をプロトコルの許容範囲内に同期してください。初回テーブル作成失敗後は再試行できます。登録時に 1 バッチ清掃し、`await registry.pruneExpired()` は期限切れ公開鍵と nonce を各 100 件まで削除します。低トラフィック時もアプリで定期実行してください。暗黙のタイマーは作らず、注入された reldb も閉じません。公開鍵の期限は BIGINT の `created_at_ms` / `expires_at_ms` に保存します。容量は `hai_crypto_transport_capacity` に永続化されるため、変更時は全ノードと保存行を調整し、registry 再作成のために有効な nonce を消さないでください。
198
225
 
199
226
  ```ts
200
227
  const registry = createReldbTransportKeyStore({ reldb, ttlSeconds: 3600, maxClients: 10000 })
@@ -256,7 +283,7 @@ const server = crypto.transport.createServer({ serverKeyRing, keyStore })
256
283
 
257
284
  空の POST も空平文の認証封装を送ります。GET/HEAD は X-Hai-Transport ヘッダー、204/304/HEAD など本文なし応答も同じヘッダーを使います。他の要求・応答は JSON 本文と X-Encrypted: true を使います。クライアントは元の要求コンテキストと HTTP status を照合し、マーカー欠落、旧形式、認証失敗では reject します。平文 endpoint は Serv/Kit の `encryption: disabled` ルールまたはホストの `excludePaths` で明示的に許可し、呼出側も通常の要求を選びます。API Client/Kit のラッパーは自身の `excludePaths` を使用します。CORS では X-Hai-Transport を許可・公開してください。鍵交換の公開鍵認証は TLS に依存し、IAM の業務認可は別に必要です。
258
285
 
259
- `transport.maxPlaintextBytes` でリクエストとレスポンスの JSON 平文容量を共通設定します。単位は UTF-8 バイト、既定値は 1 MiB、有効範囲は 1 KiB~64 MiB の整数です。クライアントは鍵交換で上限を取得し、`resolveTransportBodyLimits()` が暗号文とプロキシ容量(平文の 2 倍 + 32 KiB)を算出します。鍵交換、公開鍵、認証ヘッダー、防リプレイ制約は独立です。超過時は元ストリームを停止して reader を解放します。要求は 413、応答は暗号化された 500 容量エラーとなり、`TransportBodyTooLargeError` / `hai:crypto:070` で識別できます。 [容量配置、文件换算与升级](TRANSPORT-CAPACITY.md)。
286
+ `transport.maxPlaintextBytes` でリクエストとレスポンスの JSON 平文容量を共通設定します。単位は UTF-8 バイト、既定値は 1 MiB、有効範囲は 1 KiB~64 MiB の整数です。クライアントは鍵交換で上限を取得し、`resolveTransportBodyLimits()` が暗号文とプロキシ容量(平文の 2 倍 + 32 KiB)を算出します。鍵交換、公開鍵、認証ヘッダー、防リプレイ制約は独立です。超過時は元ストリームを停止して reader を解放します。要求は 413、応答は暗号化された 500 容量エラーとなり、`TransportBodyTooLargeError` / `hai:crypto:070` で識別できます。 [容量設定とファイルサイズの計算](TRANSPORT-CAPACITY.md)。
260
287
 
261
288
  通常業務は `transport.maxPlaintextBytes`(auto/省略は 1 MiB)を維持します。Feedback の受理・証拠・添付・ZIP ダウンロードは `routePlaintextBytes` と `feedback.budget.maxTransportBytes` で別途宣言します。`*` は完全な1パス区間のみ一致し、ホストの明示値が優先です。ハンドシェイク、adapter-node、プロキシは経路の最大値を使用します。HTTP アダプターは個別宣言された 1 MiB 超の資料経路を最大2件、通常業務を別枠で16件まで許可し、応答完了・失敗時に解放します。混雑は503 / `hai:crypto:072` を返し、業務書込は自動再送しません。
262
289
 
@@ -276,33 +303,34 @@ const text = await readTransportBody(request.body, {
276
303
  const payload: unknown = JSON.parse(text)
277
304
  ```
278
305
 
279
- #### モジュールの終了
306
+ ### モジュールの終了
280
307
 
281
308
  ```ts
282
309
  // 使用完毕后关闭,释放内部状态
283
310
  await crypto.close()
284
311
  ```
285
312
 
286
- ### エラー処理
313
+ ### セキュリティに関する注意
287
314
 
288
- すべての操作は `HaiResult<T>` を返します:
315
+ 伝送クライアントの `destroy()` は進行中のハンドシェイクを無効化して中断し、遅延結果による鍵の復活や次回ハンドシェイクの上書きを防ぎます。サーバー registry は自身の期限で回収します。
289
316
 
290
- ```ts
291
- const result = crypto.asymmetric.encrypt('data', publicKey)
292
- if (!result.success) {
293
- // result.error.code / result.error.message
294
- }
295
- ```
317
+ - **SM4 の既定の暗号化結果は構造化された暗号文**:`crypto.symmetric.encrypt(data, key)` は既定で CBC + ランダム IV を使用し、`{ mode, ciphertext, iv, encoding }` を返します。復号時はこのオブジェクトを `crypto.symmetric.decrypt(payload, key)` に渡します。
318
+ - **SM4 にはランダムな鍵を使用してください**:`crypto.symmetric.generateKey()` で鍵を生成します。このモジュールはパスワードから暗号鍵を導出する KDF を提供しません。パスワードの保存にはサーバー専用の `crypto.password.hash(password)` を使用し、パスワードやそのハッシュを直接 SM4 の鍵にしないでください。
319
+ - **IV は毎回一意にしてください**:同じ鍵で IV を再利用してはいけません。既定の CBC または `encryptWithIV()` による毎回の自動生成を推奨します。IV は暗号文と一緒に送信・保存します(公開可能で、秘密にする必要はありません)。
296
320
 
297
321
  ### 任意の連携
298
322
 
299
323
  管理画面のオンライン暗号化デモは Crypto の SM3/SM4 API を直接呼びます。SM4 はランダムな一時鍵と IV を使い、成功表示前に復号を検証します。鍵は保存しません。入力が変わると古い結果を消去し、ランダムなダイジェストや Base64 を暗号処理の結果として表示してはいけません。
300
324
 
301
- ## 本番の共有状態
325
+ ## 設定
326
+
327
+ crypto.init() に設定オブジェクトは渡しません。アルゴリズムのオプションは各呼出しに渡し、HTTP 転送は crypto.transport.createServer/createClient で設定します。サーバー鍵リングと共有 registry は明示的に注入し、秘密鍵をブラウザーや公開 YAML に含めません。
328
+
329
+ ### 本番の共有状態
302
330
 
303
331
  本番の伝送 registry は stateScope=shared と実際の Redis または PostgreSQL/MySQL が必須です。memory、および memory/SQLite を包む工場も拒否します。各ノードは同じ管理されたバージョン付き鍵セットを読み込みます。 NODE_ENV または HAI_CORE_ENV が production の場合、初期化成功前に検証します。明示的な HAI_E2E=1/HAI_E2E_MOCK=1 テストのみ除外します。 [状態・並行制御・復旧範囲(中国語)](../../.agents/instructions/distributed-state.md)。
304
332
 
305
- ## モジュール要件と明示的な上限
333
+ ### モジュール要件と明示的な上限
306
334
 
307
335
  `resolveTransportBodyLimits(limit?, { requiredPlaintextBytes? })` は limit 省略時だけ有効なモジュールの必要量から計算し、要件がなければ 1 MiB です。明示上限が必要量未満なら RangeError とし、勝手に拡張・縮小しません。範囲は 1 KiB~64 MiB、暗号文は平文の2倍+32 KiBです。Serv/Kit の `auto` は実行時に undefined へ正規化します。
308
336
 
@@ -313,6 +341,17 @@ const limits = resolveTransportBodyLimits(24 * 1024 * 1024, { requiredPlaintextB
313
341
  // plaintextBytes: 25165824; encryptedPayloadBytes: 50364416
314
342
  ```
315
343
 
344
+ ## エラー処理
345
+
346
+ 暗号化・署名・ハッシュ・パスワード操作は `HaiResult<T>` を返すため、先に `success` を確認します。鍵・IV 生成と形式検査は直接値を返し、`crypto.close()` は `Promise<void>` です。`encryptedFetch` は fetch と同様に Promise を拒否します。ストリーム読み取りや上限計算など明示的に例外を投げるツールは呼出し境界で処理してください:
347
+
348
+ ```ts
349
+ const result = crypto.asymmetric.encrypt('data', publicKey)
350
+ if (!result.success) {
351
+ // result.error.code / result.error.message
352
+ }
353
+ ```
354
+
316
355
  ## 開発とテスト
317
356
 
318
357
  ```bash
package/README.md CHANGED
@@ -4,28 +4,9 @@
4
4
 
5
5
  [框架首页](../../README.md) · 相关模块:[@h-ai/cache](../cache/README.md) · [@h-ai/core](../core/README.md) · [@h-ai/reldb](../reldb/README.md)
6
6
 
7
- ## 简介
8
-
9
7
  加密模块,提供非对称加密、哈希、对称加密与密码哈希能力。
10
8
 
11
- ## 核心功能清单
12
-
13
- - 非对称加密(密钥生成、加解密、签名验签)
14
- - 哈希(哈希、HMAC、验证)
15
- - 对称加密(ECB/CBC 模式)
16
- - 密码哈希(加盐迭代哈希)
17
- - 传输加密(`crypto.transport`:服务端管理器 + 客户端 `encryptedFetch`)
18
- - SM2/SM3/SM4 与传输支持 Node.js / 浏览器;密码哈希仅服务端
19
-
20
- ## 使用入门
21
-
22
- ### 安全声明
23
-
24
- - **SM4 默认加密返回结构化密文**:`crypto.symmetric.encrypt(data, key)` 默认使用 CBC + 随机 IV,返回 `{ mode, ciphertext, iv, encoding }`;解密时把该对象传给 `crypto.symmetric.decrypt(payload, key)`。
25
- - **SM4 使用随机密钥**:通过 `crypto.symmetric.generateKey()` 生成密钥;本模块不提供从密码派生加密密钥的 KDF。密码存储使用服务端 `crypto.password.hash(password)`,不要将密码或密码哈希直接用作 SM4 密钥。
26
- - **IV 必须唯一**:相同密钥下禁止复用 IV;推荐使用默认 CBC 或 `encryptWithIV()` 每次自动生成。密文与 IV 需一同传输与存储(IV 可公开,不必保密)。
27
-
28
- ### 快速开始
9
+ ## 快速开始
29
10
 
30
11
  并发调用 `close()` 会使尚未完成的 `init()` 失效,迟到初始化不会重新打开模块;等待该次初始化返回后可再次初始化。
31
12
 
@@ -38,7 +19,55 @@ if (!initialized.success)
38
19
  throw new Error(initialized.error.message)
39
20
  ```
40
21
 
41
- #### 非对称加密(加解密 / 签名验签)
22
+ ## 功能
23
+
24
+ ### 用途、输入与输出
25
+
26
+ 先成功初始化 crypto,再使用算法。HaiResult<T> 先判断 success,成功 data 才是 T;void 没有业务数据,Promise 必须 await。publicKey/privateKey 来自成功的密钥生成,ciphertext、signature、digest、payload 来自相应成功结果。manager 是 createServer 的成功 data,session 是 createClient 直接返回的实例;requestContext/messageContext 由 HTTP 适配器从实际请求取得,不能接收客户端自报的身份。下文给出完整加解密与 HTTP 装配。
27
+
28
+ | 入口 | 使用场景 | 输入(`—` 表示无参数) | 返回值 | 调用示例 |
29
+ | ------------------------------------- | ------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
30
+ | `crypto.init` | 准备加密能力 | `—` | `Promise<HaiResult<void>>` | `await crypto.init()` |
31
+ | `crypto.close` | 释放模块状态 | `—` | `Promise<void>` | `await crypto.close()` |
32
+ | `crypto.isInitialized` | 检查可用状态 | `—` | `boolean` | `crypto.isInitialized` |
33
+ | `crypto.asymmetric.generateKeyPair` | 生成 SM2 密钥对 | `—` | `HaiResult<{ publicKey, privateKey }>` | `crypto.asymmetric.generateKeyPair()` |
34
+ | `crypto.asymmetric.encrypt` | 用接收方公钥加密 | `data: string, publicKey: string, options?: { cipherMode, outputFormat }` | `HaiResult<string>` | `crypto.asymmetric.encrypt("orders", publicKey)` |
35
+ | `crypto.asymmetric.decrypt` | 用私钥解密 | `ciphertext: string, privateKey: string, options?` | `HaiResult<string>` | `crypto.asymmetric.decrypt(ciphertext, privateKey)` |
36
+ | `crypto.asymmetric.sign` | 为消息签名 | `data: string, privateKey: string, options?: { hash, userId }` | `HaiResult<string>` | `crypto.asymmetric.sign("orders", privateKey)` |
37
+ | `crypto.asymmetric.verify` | 核对消息签名 | `data: string, signature: string, publicKey: string, options?` | `HaiResult<boolean>` | `crypto.asymmetric.verify("orders", signature, publicKey)` |
38
+ | `crypto.asymmetric.isValidPublicKey` | 检查公钥格式 | `publicKey: string` | `boolean` | `crypto.asymmetric.isValidPublicKey(publicKey)` |
39
+ | `crypto.asymmetric.isValidPrivateKey` | 检查私钥格式 | `privateKey: string` | `boolean` | `crypto.asymmetric.isValidPrivateKey(privateKey)` |
40
+ | `crypto.hash.hash` | 计算 SM3 摘要 | `data: string / Uint8Array, options?: { inputEncoding }` | `HaiResult<string>: 64 hex` | `crypto.hash.hash("orders")` |
41
+ | `crypto.hash.hmac` | 计算带密钥的摘要 | `data: string, key: string` | `HaiResult<string>` | `crypto.hash.hmac("orders", hmacKey)` |
42
+ | `crypto.hash.verify` | 核对文本与摘要 | `data: string, expectedHash: string` | `HaiResult<boolean>` | `crypto.hash.verify("orders", digest)` |
43
+ | `crypto.symmetric.generateKey` | 生成随机 SM4 密钥 | `—` | `string: 32 hex` | `crypto.symmetric.generateKey()` |
44
+ | `crypto.symmetric.generateIV` | 生成随机初始化向量 | `—` | `string: 32 hex` | `crypto.symmetric.generateIV()` |
45
+ | `crypto.symmetric.encrypt` | 加密并保留解密信息 | `data: string, key: 32 hex, options?: { mode, iv, outputFormat }` | `HaiResult<{ mode, ciphertext, iv?, encoding }>` | `crypto.symmetric.encrypt("orders", key)` |
46
+ | `crypto.symmetric.decrypt` | 解密结构化密文 | `payload: SymmetricEncryptedPayload, key: 32 hex` | `HaiResult<string>` | `crypto.symmetric.decrypt(payload, key)` |
47
+ | `crypto.symmetric.encryptWithIV` | 每次生成新 IV 加密 | `data: string, key: 32 hex` | `HaiResult<{ mode: "cbc", ciphertext, iv, encoding }>` | `crypto.symmetric.encryptWithIV("orders", key)` |
48
+ | `crypto.symmetric.decryptWithIV` | 解密指定 IV 的密文 | `ciphertext: string, key: 32 hex, iv: 32 hex` | `HaiResult<string>` | `crypto.symmetric.decryptWithIV(ciphertext, key, iv)` |
49
+ | `crypto.symmetric.isValidKey` | 检查密钥格式 | `key: string` | `boolean` | `crypto.symmetric.isValidKey(key)` |
50
+ | `crypto.symmetric.isValidIV` | 检查 IV 格式 | `iv: string` | `boolean` | `crypto.symmetric.isValidIV(iv)` |
51
+ | `crypto.password.hash` | 服务端保存密码摘要 | `password: string, options?: { saltLength, iterations }` | `HaiResult<string>: $hai$…` | `crypto.password.hash(password)` |
52
+ | `crypto.password.verify` | 服务端验证密码 | `password: string, storedHash: string` | `HaiResult<boolean>` | `crypto.password.verify(password, storedHash)` |
53
+ | `crypto.transport.createServer` | 创建共享传输管理器 | `{ serverKeyRing, keyStore, maxPlaintextBytes?, … }` | `HaiResult<TransportEncryptionManager>` | `crypto.transport.createServer({ serverKeyRing, keyStore })` |
54
+ | `crypto.transport.createClient` | 创建加密 HTTP 会话 | `{ keyExchangeUrl: HTTPS URL, fetch?, maxPlaintextBytes? }` | `TransportClient: init / encryptedFetch / destroy` | `crypto.transport.createClient({ keyExchangeUrl: "https://api.example.com/api/v1/_hai/key-exchange" })` |
55
+ | `crypto.transport.protocol` | 复用协议路径与头名 | `—` | `TRANSPORT_PROTOCOL_T` | `crypto.transport.protocol` |
56
+ | `manager.getServerKeyId` | 读取当前服务端密钥版本 | `—` | `string` | `manager.getServerKeyId()` |
57
+ | `manager.getServerPublicKey` | 提供握手公钥 | `—` | `string` | `manager.getServerPublicKey()` |
58
+ | `manager.registerClientKey` | 登记客户端公钥 | `clientPublicKey: string` | `Promise<string>: clientId` | `await manager.registerClientKey(clientPublicKey)` |
59
+ | `manager.getClientPublicKey` | 查询共享登记的公钥 | `clientId: string` | `Promise<string / undefined>` | `await manager.getClientPublicKey(clientId)` |
60
+ | `manager.decryptRequest` | 验证并解密请求 | `payload: EncryptedRequestPayload, expected: TransportRequestContext` | `Promise<HaiResult<string>>` | `await manager.decryptRequest(encryptedRequest, requestContext)` |
61
+ | `manager.encryptResponse` | 认证并加密关联响应 | `request: TransportMessageContext, data: string, status: number` | `Promise<HaiResult<EncryptedPayload>>` | `await manager.encryptResponse(messageContext, "{}", 200)` |
62
+ | `manager.close` | 释放管理器资源 | `—` | `Promise<void>` | `await manager.close()` |
63
+ | `session.init` | 显式执行握手 | `—` | `Promise<HaiResult<void>>` | `await session.init()` |
64
+ | `session.encryptedFetch` | 按 fetch 契约调用加密接口 | `input: RequestInfo / URL, init?: RequestInit` | `Promise<Response>` | `await session.encryptedFetch("https://api.example.com/api/v1/orders")` |
65
+ | `session.ready` | 检查握手状态 | `—` | `boolean` | `session.ready()` |
66
+ | `session.destroy` | 清除本地传输会话 | `—` | `void` | `session.destroy()` |
67
+
68
+ SM2/SM3/SM4 与传输支持 Node.js / 浏览器;密码哈希仅服务端
69
+
70
+ ### 非对称加密(加解密 / 签名验签)
42
71
 
43
72
  ```ts
44
73
  // 生成密钥对
@@ -69,7 +98,7 @@ crypto.asymmetric.isValidPublicKey(publicKey) // true
69
98
  crypto.asymmetric.isValidPrivateKey(privateKey) // true
70
99
  ```
71
100
 
72
- #### 哈希(Hash / HMAC / 验证)
101
+ ### 哈希(Hash / HMAC / 验证)
73
102
 
74
103
  ```ts
75
104
  // 字符串哈希
@@ -93,7 +122,7 @@ if (hash.success) {
93
122
  }
94
123
  ```
95
124
 
96
- #### 对称加密(CBC 默认 / 结构化密文)
125
+ ### 对称加密(CBC 默认 / 结构化密文)
97
126
 
98
127
  ```ts
99
128
  // 生成随机密钥和 IV
@@ -134,7 +163,7 @@ crypto.symmetric.isValidKey(key) // true
134
163
  crypto.symmetric.isValidIV(iv) // true
135
164
  ```
136
165
 
137
- #### 密码哈希(加盐迭代)
166
+ ### 密码哈希(加盐迭代)
138
167
 
139
168
  ```ts
140
169
  // 哈希密码(输出格式: $hai$<iterations>$<salt>$<hash>)
@@ -156,7 +185,7 @@ if (hashed.success) {
156
185
  }
157
186
  ```
158
187
 
159
- #### 传输加密(crypto.transport)
188
+ ### 传输加密(crypto.transport)
160
189
 
161
190
  传输客户端 `destroy()` 使在途握手失效并取消网络等待;迟到结果不能恢复密钥或覆盖下一次握手。它只清理本地会话,服务端 registry 仍按自己的租约回收。
162
191
 
@@ -171,7 +200,7 @@ if (hashed.success) {
171
200
 
172
201
  只有在自定义运行时、测试或你确实要自己接 HTTP 协商端点时,才建议直接调用 `crypto.transport.createServer({ serverKeyRing, keyStore })` / `createClient()`。
173
202
 
174
- ##### 使用流程
203
+ #### 使用流程
175
204
 
176
205
  1. 先 `await crypto.init()`,确保 `crypto.asymmetric` 与 `crypto.symmetric` 已初始化。
177
206
  2. 服务端调用 `crypto.transport.createServer({ serverKeyRing, keyStore })` 创建 `manager`;它持有服务端密钥对,并负责保存客户端公钥。多节点部署时可注入共享 `keyStore`。
@@ -180,7 +209,7 @@ if (hashed.success) {
180
209
  5. 每次请求都发送认证封装 `{ context, direction, status, encryptedKey, ciphertext, iv, tag, signature }`;空 POST 和 GET/HEAD 也必须认证。
181
210
  6. 自定义适配器先 `await authenticateTransportRequest(manager, request)`,成功后才将 `data.plaintext` 交给业务并保存 `data.context`。
182
211
  7. 响应用 `await manager.encryptResponse(context, data, response.status)` 关联原请求;客户端校验上下文、状态和完整性后才返回明文。
183
- 8. 客户端调用 `client.destroy()` 可清空当前会话;服务端调用 `manager.close()`,或在模块级调用 `await crypto.close()`,用于释放资源。
212
+ 8. 客户端调用 `client.destroy()` 清空本地会话;服务端先 `await manager.close()` 释放自建管理器,再 `await crypto.close()` 关闭模块。模块不会替应用关闭已经创建的独立管理器,也不因此撤销服务端业务会话。
184
213
 
185
214
  > 服务端必须显式注入相同的受管 `serverKeyRing` 和共享 `keyStore`(Redis 或共享数据库)。仅共享客户端公钥不足以跨节点解密。没有隐式进程私钥或内存 registry 回退;开发/测试显式用 `createInMemoryKeyStore(capacity)`。服务端重启后重新加载同一密钥环即可,无需 sticky session。
186
215
 
@@ -194,7 +223,7 @@ if (hashed.success) {
194
223
 
195
224
  Redis/DB 的 `ttlSeconds` 省略表示不过期;显式值须为 1–2147483647 的整数秒,建议设置有限 TTL。Redis 使用原生逐键 TTL,不提供 DB 的 `maxClients` 容量限制,部署需设置内存预算且禁止驱逐有效 nonce。DB `maxClients` 为正安全整数,默认 10000,所有节点必须与持久容量记录一致;事务锁定共享容量行后计数和插入,容量满时拒绝注册,不驱逐有效公钥或 nonce。删除客户端不删除尚有效的 nonce。
196
225
 
197
- DB 使用数据库时钟生成、判断 epoch 毫秒过期时间;数据库与节点均须满足协议的时钟同步要求。首次建表失败可重试。每次注册自动清理一批;`await registry.pruneExpired()` 每次最多删除 100 条过期公钥和 100 条过期 nonce,可由应用定期调度。没有隐式后台计时器,低流量部署仍需主动调度维护;registry 不关闭应用注入的 reldb。旧公钥表的 timestamp 字段需停机迁移为 `created_at_ms` / `expires_at_ms` BIGINT,容量在 `hai_crypto_transport_capacity` 保存;容量调整须协调所有节点及持久记录,不能清空有效 nonce 来重建 registry。
226
+ DB 使用数据库时钟生成、判断 epoch 毫秒过期时间;数据库与节点均须满足协议的时钟同步要求。首次建表失败可重试。每次注册自动清理一批;`await registry.pruneExpired()` 每次最多删除 100 条过期公钥和 100 条过期 nonce,可由应用定期调度。没有隐式后台计时器,低流量部署仍需主动调度维护;registry 不关闭应用注入的 reldb。公钥过期时间保存在 BIGINT 字段 `created_at_ms` / `expires_at_ms`,容量在 `hai_crypto_transport_capacity` 保存;容量调整须协调所有节点及持久记录,不能清空有效 nonce 来重建 registry。
198
227
 
199
228
  ```ts
200
229
  const registry = createReldbTransportKeyStore({ reldb, ttlSeconds: 3600, maxClients: 10000 })
@@ -256,7 +285,7 @@ const server = crypto.transport.createServer({ serverKeyRing, keyStore })
256
285
 
257
286
  空 POST 也发送空明文的认证封装;GET/HEAD 将封装放在 X-Hai-Transport 头,204/304/HEAD 等无正文响应也使用同名头承载认证封装。其它请求/响应在 JSON 正文承载封装并带 X-Encrypted: true。客户端必须关联原请求上下文和 HTTP status,缺标记、旧格式或认证失败一律 reject;明文接口由 Serv/Kit 路径规则 `encryption: disabled` 或宿主 `excludePaths` 显式允许;调用方同步选择普通请求,API Client/Kit 包装器使用自己的 `excludePaths`。CORS 须允许并暴露 X-Hai-Transport。握手公钥的真实性依赖 TLS,不替代 IAM 业务授权。
258
287
 
259
- 业务容量通过 `transport.maxPlaintextBytes` 统一配置:默认 1 MiB,整数范围 1 KiB~64 MiB,请求和响应共用 UTF-8 明文字节预算。客户端从握手自动获取上限,密文与部署代理预算由 `resolveTransportBodyLimits()` 推导(两倍明文 + 32 KiB)。握手、公钥、认证头和防重放约束独立;超限取消原流并释放 reader,请求 413、响应为加密的 500 容量错误,客户端识别 `TransportBodyTooLargeError` / `hai:crypto:070`。 [容量配置、文件换算与升级](TRANSPORT-CAPACITY.md)。
288
+ 业务容量通过 `transport.maxPlaintextBytes` 统一配置:默认 1 MiB,整数范围 1 KiB~64 MiB,请求和响应共用 UTF-8 明文字节预算。客户端从握手自动获取上限,密文与部署代理预算由 `resolveTransportBodyLimits()` 推导(两倍明文 + 32 KiB)。握手、公钥、认证头和防重放约束独立;超限取消原流并释放 reader,请求 413、响应为加密的 500 容量错误,客户端识别 `TransportBodyTooLargeError` / `hai:crypto:070`。 [容量配置与文件换算](TRANSPORT-CAPACITY.md)。
260
289
 
261
290
  普通业务仍使用 `transport.maxPlaintextBytes`(auto/省略为 1 MiB);Feedback 的报告受理、证据、附件及 ZIP 下载路由通过 `routePlaintextBytes` 单独声明 `feedback.budget.maxTransportBytes`。路径中的 `*` 仅匹配一个完整路径段;宿主显式路由值优先。握手、adapter-node 与代理使用各路由最大容量,普通业务不会随材料预算扩大。HTTP 适配器为单独声明且大于 1 MiB 的材料路由保留最多 2 个槽,普通业务另有 16 个槽,完整响应/失败后释放;忙碌返回 503 / `hai:crypto:072`,不自动重放业务写入。
262
291
 
@@ -276,33 +305,32 @@ const text = await readTransportBody(request.body, {
276
305
  const payload: unknown = JSON.parse(text)
277
306
  ```
278
307
 
279
- #### 关闭模块
308
+ ### 关闭模块
280
309
 
281
310
  ```ts
282
311
  // 使用完毕后关闭,释放内部状态
283
312
  await crypto.close()
284
313
  ```
285
314
 
286
- ### 错误处理
287
-
288
- 所有操作返回 `HaiResult<T>`:
315
+ ### 安全声明
289
316
 
290
- ```ts
291
- const result = crypto.asymmetric.encrypt('data', publicKey)
292
- if (!result.success) {
293
- // result.error.code / result.error.message
294
- }
295
- ```
317
+ - **SM4 默认加密返回结构化密文**:`crypto.symmetric.encrypt(data, key)` 默认使用 CBC + 随机 IV,返回 `{ mode, ciphertext, iv, encoding }`;解密时把该对象传给 `crypto.symmetric.decrypt(payload, key)`。
318
+ - **SM4 使用随机密钥**:通过 `crypto.symmetric.generateKey()` 生成密钥;本模块不提供从密码派生加密密钥的 KDF。密码存储使用服务端 `crypto.password.hash(password)`,不要将密码或密码哈希直接用作 SM4 密钥。
319
+ - **IV 必须唯一**:相同密钥下禁止复用 IV;推荐使用默认 CBC 或 `encryptWithIV()` 每次自动生成。密文与 IV 需一同传输与存储(IV 可公开,不必保密)。
296
320
 
297
321
  ### 可选集成
298
322
 
299
323
  管理端在线加密演示直接调用 Crypto SM3/SM4 API,SM4 使用随机临时密钥和 IV,并在显示成功前解密校验;密钥不保存。输入改变即清空旧结果,禁止用随机摘要或 Base64 伪装算法结果。
300
324
 
301
- ## 生产共享状态
325
+ ## 配置
326
+
327
+ crypto.init() 不接收配置对象。算法选项由每次调用传入;HTTP 传输由 crypto.transport.createServer/createClient 的选项配置,密钥环和共享 registry 由服务端显式注入。输入及默认值见下文,私钥不进入浏览器或公共 YAML。
328
+
329
+ ### 生产共享状态
302
330
 
303
331
  生产传输 registry 必须声明 stateScope=shared 并实际使用 Redis 或 PostgreSQL/MySQL。memory、包装 memory 的 Redis 工厂、包装 SQLite 的 RelDB 工厂会被拒绝;各节点加载相同受管版本化密钥环。 NODE_ENV 或 HAI_CORE_ENV 为 production 时在初始化发布成功前校验;仅 HAI_E2E=1/HAI_E2E_MOCK=1 明确测试可豁免。 [状态、并发和恢复边界](../../.agents/instructions/distributed-state.md)。
304
332
 
305
- ## 模块容量与显式上限
333
+ ### 模块容量与显式上限
306
334
 
307
335
  `resolveTransportBodyLimits(limit?, { requiredPlaintextBytes? })` 只在 limit 未设置时按模块需求推导,无模块需求时仍为 1 MiB。显式上限小于模块需求时抛出 RangeError,不能静默扩大或缩小。所有值仍须在 1 KiB~64 MiB 范围;密文预算为明文两倍加 32 KiB。Serv/Kit 配置可用 `auto` 表示未显式限额,运行时将它规范化为 undefined。
308
336
 
@@ -313,6 +341,17 @@ const limits = resolveTransportBodyLimits(24 * 1024 * 1024, { requiredPlaintextB
313
341
  // limits.plaintextBytes === 25165824;limits.encryptedPayloadBytes === 50364416
314
342
  ```
315
343
 
344
+ ## 错误处理
345
+
346
+ 加解密、签名、哈希和密码操作返回 `HaiResult<T>`,先判断 `success`。随机 key/IV 与格式检测工具返回直接值;`crypto.close()` 返回 `Promise<void>`。`encryptedFetch` 按 fetch 语义拒绝 Promise,流读取、容量推导等显式抛错工具须在各自调用边界捕获:
347
+
348
+ ```ts
349
+ const result = crypto.asymmetric.encrypt('data', publicKey)
350
+ if (!result.success) {
351
+ // result.error.code / result.error.message
352
+ }
353
+ ```
354
+
316
355
  ## 开发测试
317
356
 
318
357
  ```bash
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@h-ai/crypto",
3
3
  "type": "module",
4
- "version": "1.0.0-beta.19",
4
+ "version": "1.0.0-beta.20",
5
5
  "description": "Hai Framework cryptography module (asymmetric, symmetric, and hash).",
6
6
  "license": "Apache-2.0",
7
7
  "exports": {
@@ -23,12 +23,12 @@
23
23
  "node": "^22.23.4 || ^24.22.1 || ^26.12.1"
24
24
  },
25
25
  "dependencies": {
26
- "@h-ai/core": "1.0.0-beta.19",
26
+ "@h-ai/core": "1.0.0-beta.20",
27
27
  "sm-crypto": "^0.5.8"
28
28
  },
29
29
  "optionalDependencies": {
30
- "@h-ai/cache": "1.0.0-beta.19",
31
- "@h-ai/reldb": "1.0.0-beta.19"
30
+ "@h-ai/cache": "1.0.0-beta.20",
31
+ "@h-ai/reldb": "1.0.0-beta.20"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^26.6.6",