@h-ai/crypto 0.1.0-alpha.66 → 0.1.0-alpha.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +86 -31
- package/README.ja.md +86 -31
- package/README.md +88 -33
- package/dist/index.d.ts +1625 -173
- package/dist/index.js +719 -230
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/README.en.md
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
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
|
+
|
|
7
9
|
A cryptography module for asymmetric encryption, hashing, symmetric encryption, and password hashing.
|
|
8
10
|
|
|
9
|
-
##
|
|
11
|
+
## Core features
|
|
10
12
|
|
|
11
13
|
- Asymmetric cryptography (key generation, encryption/decryption, signing/verification)
|
|
12
14
|
- Hashing (hash, HMAC, verification)
|
|
@@ -15,22 +17,26 @@ A cryptography module for asymmetric encryption, hashing, symmetric encryption,
|
|
|
15
17
|
- Transport encryption (`crypto.transport`: server manager + client `encryptedFetch`)
|
|
16
18
|
- Shared server/browser support (Node.js / browser)
|
|
17
19
|
|
|
18
|
-
##
|
|
20
|
+
## Getting started
|
|
21
|
+
|
|
22
|
+
### Security notice
|
|
19
23
|
|
|
20
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.
|
|
21
|
-
- **
|
|
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.
|
|
22
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.
|
|
23
27
|
|
|
24
|
-
|
|
28
|
+
### Quick start
|
|
25
29
|
|
|
26
30
|
```ts
|
|
27
31
|
import { crypto } from '@h-ai/crypto'
|
|
28
32
|
|
|
29
33
|
// 初始化(使用前必须调用)
|
|
30
|
-
await crypto.init()
|
|
34
|
+
const initialized = await crypto.init()
|
|
35
|
+
if (!initialized.success)
|
|
36
|
+
throw new Error(initialized.error.message)
|
|
31
37
|
```
|
|
32
38
|
|
|
33
|
-
|
|
39
|
+
#### Asymmetric cryptography (encryption / signing)
|
|
34
40
|
|
|
35
41
|
```ts
|
|
36
42
|
// 生成密钥对
|
|
@@ -61,7 +67,7 @@ crypto.asymmetric.isValidPublicKey(publicKey) // true
|
|
|
61
67
|
crypto.asymmetric.isValidPrivateKey(privateKey) // true
|
|
62
68
|
```
|
|
63
69
|
|
|
64
|
-
|
|
70
|
+
#### Hashing (Hash / HMAC / verification)
|
|
65
71
|
|
|
66
72
|
```ts
|
|
67
73
|
// 字符串哈希
|
|
@@ -85,7 +91,7 @@ if (hash.success) {
|
|
|
85
91
|
}
|
|
86
92
|
```
|
|
87
93
|
|
|
88
|
-
|
|
94
|
+
#### Symmetric encryption (default CBC / structured payload)
|
|
89
95
|
|
|
90
96
|
```ts
|
|
91
97
|
// 生成随机密钥和 IV
|
|
@@ -121,16 +127,12 @@ if (ecbEnc.success) {
|
|
|
121
127
|
// 输出 base64 格式(ciphertext 字段为 base64,encoding 字段会标明)
|
|
122
128
|
const b64Enc = crypto.symmetric.encrypt('data', key, { outputFormat: 'base64' })
|
|
123
129
|
|
|
124
|
-
// ⚠️ 已弃用:从密码派生密钥(仅为单次 SM3 哈希,不是 KDF,禁止用于密码存储场景)
|
|
125
|
-
// 密码散列请用 crypto.password.hash();如需从密码派生密钥,请在应用层自行实现 PBKDF2 / scrypt / Argon2
|
|
126
|
-
const derivedKey = crypto.symmetric.deriveKey('my-password', 'random-salt')
|
|
127
|
-
|
|
128
130
|
// 校验密钥/IV 格式
|
|
129
131
|
crypto.symmetric.isValidKey(key) // true
|
|
130
132
|
crypto.symmetric.isValidIV(iv) // true
|
|
131
133
|
```
|
|
132
134
|
|
|
133
|
-
|
|
135
|
+
#### Password hashing (salted iterations)
|
|
134
136
|
|
|
135
137
|
```ts
|
|
136
138
|
// 哈希密码(输出格式: $hai$<iterations>$<salt>$<hash>)
|
|
@@ -152,39 +154,53 @@ if (hashed.success) {
|
|
|
152
154
|
}
|
|
153
155
|
```
|
|
154
156
|
|
|
155
|
-
|
|
157
|
+
#### Transport encryption (crypto.transport)
|
|
158
|
+
|
|
159
|
+
See the [Transport v2 wire protocol](TRANSPORT-PROTOCOL.md) for canonical encoding, verification order and replay persistence.
|
|
156
160
|
|
|
157
161
|
The `crypto.transport` namespace provides transport encryption, allowing `@h-ai/serv`, `@h-ai/kit`, and `@h-ai/api-client` to share protocol constants and payload formats.
|
|
158
162
|
|
|
159
163
|
For typical applications, use the higher-level integrations:
|
|
160
164
|
|
|
161
|
-
- Server: `serv.createApp({ transport: { crypto } })` or `kit.createHandle({ crypto: { crypto, transport:
|
|
165
|
+
- Server: `serv.createApp({ transport: { crypto, serverKeyRing, keyStore } })` or `kit.createHandle({ crypto: { crypto, transport: { serverKeyRing, keyStore } } })`.
|
|
162
166
|
- Client: `apiClient.init({ transport: { crypto } })` or `kit.client.create({ transport: { crypto } })`.
|
|
163
167
|
|
|
164
|
-
Call `crypto.transport.createServer()` / `createClient()` directly only for custom runtimes, tests, or manually integrated HTTP key exchange endpoints.
|
|
168
|
+
Call `crypto.transport.createServer({ serverKeyRing, keyStore })` / `createClient()` directly only for custom runtimes, tests, or manually integrated HTTP key exchange endpoints.
|
|
165
169
|
|
|
166
|
-
|
|
170
|
+
##### Workflow
|
|
167
171
|
|
|
168
172
|
1. Call `await crypto.init()` to initialize `crypto.asymmetric` and `crypto.symmetric`.
|
|
169
|
-
2. On the server, create a `manager` with `crypto.transport.createServer()`. It holds the server key pair and stores client public keys. Inject a shared `keyStore` for multi-node deployments.
|
|
170
|
-
3. Expose a POST key exchange endpoint: accept `{ clientPublicKey }`, register it with `manager.registerClientKey()`, and return `{ serverPublicKey: manager.getServerPublicKey(), clientId }`.
|
|
173
|
+
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.
|
|
174
|
+
3. Expose a POST key exchange endpoint: accept `{ clientPublicKey }`, register it with `manager.registerClientKey()`, and return `{ serverKeyId: manager.getServerKeyId(), serverPublicKey: manager.getServerPublicKey(), clientId }`.
|
|
171
175
|
4. On the client, create a session with `crypto.transport.createClient({ keyExchangeUrl })`. The first `client.init()` or `client.encryptedFetch()` automatically performs key exchange.
|
|
172
|
-
5.
|
|
173
|
-
6.
|
|
174
|
-
7.
|
|
176
|
+
5. Every request sends an authenticated envelope `{ context, direction, status, encryptedKey, ciphertext, iv, tag, signature }`, including empty POST and GET/HEAD.
|
|
177
|
+
6. Custom adapters first await `authenticateTransportRequest(manager, request)`. Only successful `data.plaintext` reaches business code; retain `data.context`.
|
|
178
|
+
7. Encrypt responses with `await manager.encryptResponse(context, data, response.status)`; clients verify context, status and authenticity before returning plaintext.
|
|
175
179
|
8. Clear a client session with `client.destroy()`. Release server resources with `manager.close()` or module-level `await crypto.close()`.
|
|
176
180
|
|
|
177
|
-
>
|
|
181
|
+
> 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.
|
|
178
182
|
|
|
179
183
|
For shared storage, reuse the provider factories exported from the `@h-ai/crypto` root entry:
|
|
180
184
|
|
|
181
185
|
- `createInMemoryKeyStore(maxClients)`: explicitly create the default memory store for tests or custom capacity.
|
|
182
186
|
- `createRedisTransportKeyStore({ cache, ttlSeconds? })`: reuse `@h-ai/cache`, typically with Redis for sharing across nodes.
|
|
183
|
-
- `createReldbTransportKeyStore({ reldb, ttlSeconds? })`: reuse `@h-ai/reldb`,
|
|
187
|
+
- `createReldbTransportKeyStore({ reldb, ttlSeconds?, maxClients? })`: reuse initialized `@h-ai/reldb`, create key, nonce and capacity tables, and return a `ReldbTransportKeyStore` with `pruneExpired()`.
|
|
188
|
+
|
|
189
|
+
Registration validates the encoding and SM2 curve point before writing. Only 128 hex characters or `04` followed by 128 hex characters are accepted; compressed, infinity and off-curve points are rejected. The pure `isValidSM2PublicKey(value)` helper requires no initialization. serv/kit reject invalid keys with HTTP 400 before accessing the registry.
|
|
190
|
+
|
|
191
|
+
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.
|
|
192
|
+
|
|
193
|
+
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.
|
|
184
194
|
|
|
185
195
|
```ts
|
|
186
|
-
|
|
187
|
-
|
|
196
|
+
const registry = createReldbTransportKeyStore({ reldb, ttlSeconds: 3600, maxClients: 10000 })
|
|
197
|
+
// 由应用的定期维护调度器调用;每批有界,故障后由调度器重试。
|
|
198
|
+
const removed = await registry.pruneExpired()
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
// 服务端:通常由 serv.createApp({ transport: { crypto, serverKeyRing, keyStore } }) 或 kit.createHandle({ crypto }) 内部调用
|
|
203
|
+
const server = crypto.transport.createServer({ serverKeyRing, keyStore })
|
|
188
204
|
if (!server.success)
|
|
189
205
|
throw new Error(server.error.message)
|
|
190
206
|
|
|
@@ -207,6 +223,7 @@ await crypto.init()
|
|
|
207
223
|
await cache.init({ type: 'redis', host: '127.0.0.1', port: 6379 })
|
|
208
224
|
|
|
209
225
|
const server = crypto.transport.createServer({
|
|
226
|
+
serverKeyRing,
|
|
210
227
|
keyStore: createRedisTransportKeyStore({ cache, ttlSeconds: 3600 }),
|
|
211
228
|
})
|
|
212
229
|
if (!server.success)
|
|
@@ -217,14 +234,46 @@ if (!server.success)
|
|
|
217
234
|
|
|
218
235
|
Access protocol constants through `crypto.transport.protocol` (or `TRANSPORT_PROTOCOL`): `X-Client-Id`, `X-Encrypted`, and the default `/_hai/key-exchange`.
|
|
219
236
|
|
|
220
|
-
|
|
237
|
+
Both `serverKeyRing` and `keyStore` are required by `createServer({ serverKeyRing, keyStore })`. A ring is `{ activeKeyId, keys: { [id]: { publicKey, privateKey } } }`; IDs contain 1–64 letters, digits, underscores or hyphens. Creation verifies key pairing and captures a snapshot; invalid/missing configuration returns a sanitized HaiResult error. In these examples, `keyStore` is a connected Redis/shared-database registry; memory is an explicit development/test choice.
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
import { parseTransportServerKeyRing } from '@h-ai/crypto'
|
|
241
|
+
|
|
242
|
+
const ring = parseTransportServerKeyRing(process.env.HAI_CRYPTO_TRANSPORT_SERVERKEYRING)
|
|
243
|
+
if (!ring.success)
|
|
244
|
+
throw new Error(ring.error.message)
|
|
245
|
+
const serverKeyRing = ring.data
|
|
246
|
+
const server = crypto.transport.createServer({ serverKeyRing, keyStore })
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Key exchange returns `{ serverKeyId, serverPublicKey, clientId }`. Encrypted requests carry `serverKeyId`; the server selects that private key and rejects unknown/revoked IDs without trying other keys. Responses use the client's public key. Rotate by distributing both old and new versions to every node, then switching `activeKeyId` and rebuilding managers during rollout. Retain the old version through the transition, then remove it from every node. Old clients must call `destroy()` and negotiate again; do not automatically replay non-idempotent operations. Distribute the same ring through a Secret Manager or server-only private environment. Never put it in ordinary YAML, PUBLIC_/VITE_ variables, logs or public responses. CLI project creation provisions one independent development ring in ignored `.env.local`; runtime does not generate keys. Deploy with target-managed secrets.
|
|
250
|
+
|
|
251
|
+
Only authenticated protocol v2 is accepted. Independent random SM4-CBC and HMAC-SM3 keys are wrapped with SM2; verify the sender signature and MAC before CBC decryption. Clients sign requests and the selected server key version signs responses. Authentication covers version, direction, serverKeyId, clientId, a random 128-bit requestId acting as the nonce, issuedAt, HTTP method, the SM3 pathHash of pathname plus the original search, and response status. Requests have a five-minute lifetime and at most 30 seconds of future clock skew. The shared registry atomically consumes (clientId, requestId), retaining it until at least issuedAt plus six minutes to cover node skew; storage failures reject requests. Synchronize node clocks and use a Redis noeviction/durable policy that preserves live nonces. Transport replay prevention does not replace business idempotency keys.
|
|
252
|
+
|
|
253
|
+
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. Exclude plaintext endpoints explicitly with excludePaths. CORS must allow and expose X-Hai-Transport. TLS authenticates handshake keys; this protocol does not replace IAM authorization.
|
|
254
|
+
|
|
255
|
+
serv, kit and the client share `TRANSPORT_BODY_LIMITS`: 16 KiB key exchange JSON, 4 KiB per public key, 1 MiB plaintext, and 2 MiB + 32 KiB encrypted JSON. All limits count actual UTF-8 bytes. Client key exchange has a 30-second timeout. Exceeding a limit, invalid UTF-8 or cancellation stops the original stream and releases its reader without cloning/teeing it. Caller signals remain active; cancelling one request does not cancel another request's shared handshake. Invalid encrypted responses reject instead of returning raw ciphertext.
|
|
256
|
+
|
|
257
|
+
Custom HTTP adapters can import the pure `readTransportBody(stream, { maxBytes, signal? })` helper from the root entry without `crypto.init()`. Pass the original stream, rebuild Request/Response after reading, and clear stale Content-Length/Content-Encoding. The helper throws RangeError for byte limits, TypeError for invalid UTF-8, and preserves the original read/abort failure.
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
import { readTransportBody, TRANSPORT_BODY_LIMITS } from '@h-ai/crypto'
|
|
261
|
+
|
|
262
|
+
const text = await readTransportBody(request.body, {
|
|
263
|
+
maxBytes: TRANSPORT_BODY_LIMITS.keyExchangeBytes,
|
|
264
|
+
signal: request.signal,
|
|
265
|
+
})
|
|
266
|
+
const payload: unknown = JSON.parse(text)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
#### Close the module
|
|
221
270
|
|
|
222
271
|
```ts
|
|
223
272
|
// 使用完毕后关闭,释放内部状态
|
|
224
273
|
await crypto.close()
|
|
225
274
|
```
|
|
226
275
|
|
|
227
|
-
|
|
276
|
+
### Error handling
|
|
228
277
|
|
|
229
278
|
All operations return `HaiResult<T>`:
|
|
230
279
|
|
|
@@ -235,7 +284,15 @@ if (!result.success) {
|
|
|
235
284
|
}
|
|
236
285
|
```
|
|
237
286
|
|
|
238
|
-
|
|
287
|
+
### Optional integrations
|
|
288
|
+
|
|
289
|
+
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.
|
|
290
|
+
|
|
291
|
+
## Production shared state
|
|
292
|
+
|
|
293
|
+
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)。
|
|
294
|
+
|
|
295
|
+
## Development and testing
|
|
239
296
|
|
|
240
297
|
```bash
|
|
241
298
|
pnpm --filter @h-ai/crypto test
|
|
@@ -244,5 +301,3 @@ pnpm --filter @h-ai/crypto test
|
|
|
244
301
|
## License
|
|
245
302
|
|
|
246
303
|
Apache-2.0
|
|
247
|
-
|
|
248
|
-
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.
|
package/README.ja.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
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
|
+
|
|
7
9
|
非対称暗号、ハッシュ、対称暗号、パスワードハッシュを提供する暗号モジュールです。
|
|
8
10
|
|
|
9
11
|
## 主な機能
|
|
@@ -15,22 +17,26 @@
|
|
|
15
17
|
- 通信暗号化(`crypto.transport`:サーバー管理機能 + クライアントの `encryptedFetch`)
|
|
16
18
|
- サーバー・ブラウザー共通(Node.js / ブラウザー)
|
|
17
19
|
|
|
18
|
-
##
|
|
20
|
+
## 使い始める
|
|
21
|
+
|
|
22
|
+
### セキュリティに関する注意
|
|
19
23
|
|
|
20
24
|
- **SM4 の既定の暗号化結果は構造化された暗号文**:`crypto.symmetric.encrypt(data, key)` は既定で CBC + ランダム IV を使用し、`{ mode, ciphertext, iv, encoding }` を返します。復号時はこのオブジェクトを `crypto.symmetric.decrypt(payload, key)` に渡します。
|
|
21
|
-
- **SM4
|
|
25
|
+
- **SM4 にはランダムな鍵を使用してください**:`crypto.symmetric.generateKey()` で鍵を生成します。このモジュールはパスワードから暗号鍵を導出する KDF を提供しません。パスワードの保存にはサーバー専用の `crypto.password.hash(password)` を使用し、パスワードやそのハッシュを直接 SM4 の鍵にしないでください。
|
|
22
26
|
- **IV は毎回一意にしてください**:同じ鍵で IV を再利用してはいけません。既定の CBC または `encryptWithIV()` による毎回の自動生成を推奨します。IV は暗号文と一緒に送信・保存します(公開可能で、秘密にする必要はありません)。
|
|
23
27
|
|
|
24
|
-
|
|
28
|
+
### クイックスタート
|
|
25
29
|
|
|
26
30
|
```ts
|
|
27
31
|
import { crypto } from '@h-ai/crypto'
|
|
28
32
|
|
|
29
33
|
// 初始化(使用前必须调用)
|
|
30
|
-
await crypto.init()
|
|
34
|
+
const initialized = await crypto.init()
|
|
35
|
+
if (!initialized.success)
|
|
36
|
+
throw new Error(initialized.error.message)
|
|
31
37
|
```
|
|
32
38
|
|
|
33
|
-
|
|
39
|
+
#### 非対称暗号(暗号化・復号 / 署名・検証)
|
|
34
40
|
|
|
35
41
|
```ts
|
|
36
42
|
// 生成密钥对
|
|
@@ -61,7 +67,7 @@ crypto.asymmetric.isValidPublicKey(publicKey) // true
|
|
|
61
67
|
crypto.asymmetric.isValidPrivateKey(privateKey) // true
|
|
62
68
|
```
|
|
63
69
|
|
|
64
|
-
|
|
70
|
+
#### ハッシュ(Hash / HMAC / 検証)
|
|
65
71
|
|
|
66
72
|
```ts
|
|
67
73
|
// 字符串哈希
|
|
@@ -85,7 +91,7 @@ if (hash.success) {
|
|
|
85
91
|
}
|
|
86
92
|
```
|
|
87
93
|
|
|
88
|
-
|
|
94
|
+
#### 対称暗号(既定の CBC / 構造化暗号文)
|
|
89
95
|
|
|
90
96
|
```ts
|
|
91
97
|
// 生成随机密钥和 IV
|
|
@@ -121,16 +127,12 @@ if (ecbEnc.success) {
|
|
|
121
127
|
// 输出 base64 格式(ciphertext 字段为 base64,encoding 字段会标明)
|
|
122
128
|
const b64Enc = crypto.symmetric.encrypt('data', key, { outputFormat: 'base64' })
|
|
123
129
|
|
|
124
|
-
// ⚠️ 已弃用:从密码派生密钥(仅为单次 SM3 哈希,不是 KDF,禁止用于密码存储场景)
|
|
125
|
-
// 密码散列请用 crypto.password.hash();如需从密码派生密钥,请在应用层自行实现 PBKDF2 / scrypt / Argon2
|
|
126
|
-
const derivedKey = crypto.symmetric.deriveKey('my-password', 'random-salt')
|
|
127
|
-
|
|
128
130
|
// 校验密钥/IV 格式
|
|
129
131
|
crypto.symmetric.isValidKey(key) // true
|
|
130
132
|
crypto.symmetric.isValidIV(iv) // true
|
|
131
133
|
```
|
|
132
134
|
|
|
133
|
-
|
|
135
|
+
#### パスワードハッシュ(ソルト付き反復)
|
|
134
136
|
|
|
135
137
|
```ts
|
|
136
138
|
// 哈希密码(输出格式: $hai$<iterations>$<salt>$<hash>)
|
|
@@ -152,39 +154,53 @@ if (hashed.success) {
|
|
|
152
154
|
}
|
|
153
155
|
```
|
|
154
156
|
|
|
155
|
-
|
|
157
|
+
#### 通信暗号化(crypto.transport)
|
|
158
|
+
|
|
159
|
+
正規化、検証順序、nonce 保存は [Transport v2 プロトコル](TRANSPORT-PROTOCOL.md) を参照してください。
|
|
156
160
|
|
|
157
161
|
通信暗号化は `crypto.transport` 名前空間で提供し、`@h-ai/serv`、`@h-ai/kit`、`@h-ai/api-client` で同じプロトコル定数とペイロード形式を再利用します。
|
|
158
162
|
|
|
159
163
|
通常のアプリケーションでは、上位モジュールの統合機能を優先してください:
|
|
160
164
|
|
|
161
|
-
- サーバー:`serv.createApp({ transport: { crypto } })` または `kit.createHandle({ crypto: { crypto, transport:
|
|
165
|
+
- サーバー:`serv.createApp({ transport: { crypto, serverKeyRing, keyStore } })` または `kit.createHandle({ crypto: { crypto, transport: { serverKeyRing, keyStore } } })`。
|
|
162
166
|
- クライアント:`apiClient.init({ transport: { crypto } })` または `kit.client.create({ transport: { crypto } })`。
|
|
163
167
|
|
|
164
|
-
`crypto.transport.createServer()` / `createClient()` の直接呼び出しは、独自ランタイム、テスト、HTTP 鍵交換 endpoint を自分で接続する場合に限って推奨します。
|
|
168
|
+
`crypto.transport.createServer({ serverKeyRing, keyStore })` / `createClient()` の直接呼び出しは、独自ランタイム、テスト、HTTP 鍵交換 endpoint を自分で接続する場合に限って推奨します。
|
|
165
169
|
|
|
166
|
-
|
|
170
|
+
##### 使用手順
|
|
167
171
|
|
|
168
172
|
1. `await crypto.init()` を呼び、`crypto.asymmetric` と `crypto.symmetric` を初期化します。
|
|
169
|
-
2. サーバーで `crypto.transport.createServer()` を呼び、`manager` を作成します。サーバー鍵ペアとクライアント公開鍵を管理します。複数ノードでは共有 `keyStore` を注入できます。
|
|
170
|
-
3. POST の鍵交換 endpoint を用意します。`{ clientPublicKey }` を受け取り、`manager.registerClientKey()` で登録し、`{ serverPublicKey: manager.getServerPublicKey(), clientId }` を返します。
|
|
173
|
+
2. サーバーで `crypto.transport.createServer({ serverKeyRing, keyStore })` を呼び、`manager` を作成します。サーバー鍵ペアとクライアント公開鍵を管理します。複数ノードでは共有 `keyStore` を注入できます。
|
|
174
|
+
3. POST の鍵交換 endpoint を用意します。`{ clientPublicKey }` を受け取り、`manager.registerClientKey()` で登録し、`{ serverKeyId: manager.getServerKeyId(), serverPublicKey: manager.getServerPublicKey(), clientId }` を返します。
|
|
171
175
|
4. クライアントで `crypto.transport.createClient({ keyExchangeUrl })` によりセッションを作成します。最初の `client.init()` または `client.encryptedFetch()` で鍵交換が自動実行されます。
|
|
172
|
-
5.
|
|
173
|
-
6.
|
|
174
|
-
7.
|
|
176
|
+
5. 空の POST と GET/HEAD を含め、すべての要求が認証封装 `{ context, direction, status, encryptedKey, ciphertext, iv, tag, signature }` を送ります。
|
|
177
|
+
6. 独自アダプターは先に `await authenticateTransportRequest(manager, request)` を実行し、成功時のみ `data.plaintext` を業務へ渡し、`data.context` を保持します。
|
|
178
|
+
7. 応答は `await manager.encryptResponse(context, data, response.status)` で元要求に関連付け、クライアントがコンテキスト・status・完全性を検証してから平文を返します。
|
|
175
179
|
8. `client.destroy()` でクライアントセッションを消去します。サーバー側のリソースは `manager.close()` またはモジュール単位の `await crypto.close()` で解放します。
|
|
176
180
|
|
|
177
|
-
>
|
|
181
|
+
> サーバーには同じ管理済み `serverKeyRing` と共有 `keyStore`(Redis または共有 DB)を明示的に渡します。クライアント公開鍵の共有だけではノード間で復号できません。暗黙のプロセス鍵・メモリー registry はなく、開発・テストでは `createInMemoryKeyStore(capacity)` を明示します。再起動後は同じ鍵環を読み込み、sticky session は不要です。
|
|
178
182
|
|
|
179
183
|
共有ストレージには、`@h-ai/crypto` のルート入口からエクスポートされる Provider ファクトリーを再利用できます:
|
|
180
184
|
|
|
181
185
|
- `createInMemoryKeyStore(maxClients)`:テストや容量指定用に、既定のメモリーストアを明示的に作成。
|
|
182
186
|
- `createRedisTransportKeyStore({ cache, ttlSeconds? })`:`@h-ai/cache` を再利用。通常は Redis Provider と組み合わせ、複数ノードで共有。
|
|
183
|
-
- `createReldbTransportKeyStore({ reldb, ttlSeconds? })
|
|
187
|
+
- `createReldbTransportKeyStore({ reldb, ttlSeconds?, maxClients? })`:初期化済みの `@h-ai/reldb` を再利用し、公開鍵・nonce・容量テーブルを自動作成。`pruneExpired()` を備える `ReldbTransportKeyStore` を返します。
|
|
188
|
+
|
|
189
|
+
登録前に公開鍵の形式と SM2 曲線上の点を検証します。128 文字の hex、または `04` + 128 文字の hex のみ受け付け、圧縮点・無限遠点・曲線外の点は拒否します。純関数 `isValidSM2PublicKey(value)` は初期化不要で、serv/kit は registry に書く前に不正な鍵を HTTP 400 で拒否します。
|
|
190
|
+
|
|
191
|
+
Redis/DB の `ttlSeconds` を省略すると無期限です。指定時は 1–2147483647 の整数秒とし、有限 TTL を推奨します。Redis はネイティブのキー TTL を使い、DB の `maxClients` 上限は提供しません。メモリー予算を設定し、有効な nonce を追い出さないでください。DB の `maxClients` は正の安全な整数、既定 10000 で、全ノードと永続容量レコードが一致する必要があります。登録はトランザクション内で共有容量行をロックし、件数確認後に挿入します。満杯では登録を拒否し、有効な公開鍵や nonce を削除しません。クライアント削除時も有効な nonce は保持します。
|
|
192
|
+
|
|
193
|
+
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 を消さないでください。
|
|
184
194
|
|
|
185
195
|
```ts
|
|
186
|
-
|
|
187
|
-
|
|
196
|
+
const registry = createReldbTransportKeyStore({ reldb, ttlSeconds: 3600, maxClients: 10000 })
|
|
197
|
+
// アプリの定期メンテナンスから呼び出し、失敗時はスケジューラーで再試行する。
|
|
198
|
+
const removed = await registry.pruneExpired()
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
// 服务端:通常由 serv.createApp({ transport: { crypto, serverKeyRing, keyStore } }) 或 kit.createHandle({ crypto }) 内部调用
|
|
203
|
+
const server = crypto.transport.createServer({ serverKeyRing, keyStore })
|
|
188
204
|
if (!server.success)
|
|
189
205
|
throw new Error(server.error.message)
|
|
190
206
|
|
|
@@ -207,6 +223,7 @@ await crypto.init()
|
|
|
207
223
|
await cache.init({ type: 'redis', host: '127.0.0.1', port: 6379 })
|
|
208
224
|
|
|
209
225
|
const server = crypto.transport.createServer({
|
|
226
|
+
serverKeyRing,
|
|
210
227
|
keyStore: createRedisTransportKeyStore({ cache, ttlSeconds: 3600 }),
|
|
211
228
|
})
|
|
212
229
|
if (!server.success)
|
|
@@ -217,14 +234,46 @@ if (!server.success)
|
|
|
217
234
|
|
|
218
235
|
プロトコル定数は `crypto.transport.protocol`(または `TRANSPORT_PROTOCOL`)で取得できます:`X-Client-Id`、`X-Encrypted`、既定の `/_hai/key-exchange`。
|
|
219
236
|
|
|
220
|
-
|
|
237
|
+
`createServer({ serverKeyRing, keyStore })` は両方とも必須です。鍵環は `{ activeKeyId, keys: { [id]: { publicKey, privateKey } } }` で、ID は英数字・下線・ハイフンの 1–64 文字です。作成時に鍵の組を検証しスナップショットを保持します。不正・未設定の値は機密情報を含まない HaiResult エラーになります。例の `keyStore` は接続済み Redis/共有 DB の registry です。メモリーは開発・テストで明示的に使用します。
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
import { parseTransportServerKeyRing } from '@h-ai/crypto'
|
|
241
|
+
|
|
242
|
+
const ring = parseTransportServerKeyRing(process.env.HAI_CRYPTO_TRANSPORT_SERVERKEYRING)
|
|
243
|
+
if (!ring.success)
|
|
244
|
+
throw new Error(ring.error.message)
|
|
245
|
+
const serverKeyRing = ring.data
|
|
246
|
+
const server = crypto.transport.createServer({ serverKeyRing, keyStore })
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
鍵交換は `{ serverKeyId, serverPublicKey, clientId }` を返し、暗号化リクエストは `serverKeyId` を送ります。サーバーは指定した秘密鍵だけを選び、不明・失効 ID は他の鍵を試さず拒否します。応答はクライアント公開鍵で暗号化します。更新時は全ノードに新旧の鍵を先に配布し、続いて `activeKeyId` を切り替えて manager を順次再構築します。移行期間後に古い ID を全ノードから削除します。旧クライアントは `destroy()` 後に再交渉し、非冪等処理を自動再送しません。同じ鍵環を Secret Manager またはサーバー専用の非公開環境から配布し、通常の YAML、PUBLIC_/VITE_、ログ、公開応答には含めないでください。CLI はプロジェクト作成時だけ独立した開発用鍵環を Git 対象外の `.env.local` に作成します。実行時には生成せず、本番には対象環境で管理する鍵を配布します。
|
|
250
|
+
|
|
251
|
+
認証済み v2 のみを受け付けます。独立したランダム SM4-CBC 鍵と HMAC-SM3 鍵を SM2 で包み、送信者署名、MAC の順に検証してから CBC を復号します。要求はクライアント秘密鍵、応答は選択されたサーバー鍵バージョンで署名します。version、direction、serverKeyId、clientId、nonce を兼ねるランダム 128 ビット requestId、issuedAt、HTTP method、pathname と元の search の SM3 pathHash、応答 status を認証します。要求の有効期限は 5 分、未来方向の時計差は最大 30 秒です。共有 registry が (clientId, requestId) を原子的に消費し、ノード間の時計差を含め issuedAt + 6 分まで保持します。保存障害では拒否します。時計同期と Redis の noeviction/永続化により有効な nonce を保持してください。業務の再試行には別の冪等キーが必要です。
|
|
252
|
+
|
|
253
|
+
空の POST も空平文の認証封装を送ります。GET/HEAD は X-Hai-Transport ヘッダー、204/304/HEAD など本文なし応答も同じヘッダーを使います。他の要求・応答は JSON 本文と X-Encrypted: true を使います。クライアントは元の要求コンテキストと HTTP status を照合し、マーカー欠落、旧形式、認証失敗では reject します。平文 endpoint は excludePaths で明示的に除外し、CORS では X-Hai-Transport を許可・公開してください。鍵交換の公開鍵認証は TLS に依存し、IAM の業務認可は別に必要です。
|
|
254
|
+
|
|
255
|
+
serv、kit、クライアントは `TRANSPORT_BODY_LIMITS` を共有します。鍵交換 JSON は 16 KiB、公開鍵は 1 個あたり 4 KiB、平文は 1 MiB、暗号文 JSON は 2 MiB + 32 KiB です。すべて実際の UTF-8 バイト数で計算します。クライアントの鍵交換は最大 30 秒です。超過、不正 UTF-8、キャンセル時は元のストリームを停止して reader を解放し、clone/tee は作りません。呼び出し元の signal を維持し、1 件のキャンセルは他のリクエストと共有する鍵交換に影響しません。不正な暗号化応答は生の暗号文を返さず reject します。
|
|
256
|
+
|
|
257
|
+
独自 HTTP アダプターではルート入口の純粋関数 `readTransportBody(stream, { maxBytes, signal? })` を `crypto.init()` なしで利用できます。元のストリームを渡し、読み取り後は Request/Response を再構築して古い Content-Length/Content-Encoding を削除してください。上限超過は RangeError、不正 UTF-8 は TypeError、読み取り例外・signal.reason は元の理由で拒否します。ストリームのキャンセル処理が失敗しても元の例外を維持します。
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
import { readTransportBody, TRANSPORT_BODY_LIMITS } from '@h-ai/crypto'
|
|
261
|
+
|
|
262
|
+
const text = await readTransportBody(request.body, {
|
|
263
|
+
maxBytes: TRANSPORT_BODY_LIMITS.keyExchangeBytes,
|
|
264
|
+
signal: request.signal,
|
|
265
|
+
})
|
|
266
|
+
const payload: unknown = JSON.parse(text)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
#### モジュールの終了
|
|
221
270
|
|
|
222
271
|
```ts
|
|
223
272
|
// 使用完毕后关闭,释放内部状态
|
|
224
273
|
await crypto.close()
|
|
225
274
|
```
|
|
226
275
|
|
|
227
|
-
|
|
276
|
+
### エラー処理
|
|
228
277
|
|
|
229
278
|
すべての操作は `HaiResult<T>` を返します:
|
|
230
279
|
|
|
@@ -235,14 +284,20 @@ if (!result.success) {
|
|
|
235
284
|
}
|
|
236
285
|
```
|
|
237
286
|
|
|
238
|
-
|
|
287
|
+
### 任意の連携
|
|
288
|
+
|
|
289
|
+
管理画面のオンライン暗号化デモは Crypto の SM3/SM4 API を直接呼びます。SM4 はランダムな一時鍵と IV を使い、成功表示前に復号を検証します。鍵は保存しません。入力が変わると古い結果を消去し、ランダムなダイジェストや Base64 を暗号処理の結果として表示してはいけません。
|
|
290
|
+
|
|
291
|
+
## 本番の共有状態
|
|
292
|
+
|
|
293
|
+
本番の伝送 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)。
|
|
294
|
+
|
|
295
|
+
## 開発とテスト
|
|
239
296
|
|
|
240
297
|
```bash
|
|
241
298
|
pnpm --filter @h-ai/crypto test
|
|
242
299
|
```
|
|
243
300
|
|
|
244
|
-
##
|
|
301
|
+
## ライセンス
|
|
245
302
|
|
|
246
303
|
Apache-2.0
|
|
247
|
-
|
|
248
|
-
管理画面のオンライン暗号化デモは Crypto の SM3/SM4 API を直接呼びます。SM4 はランダムな一時鍵と IV を使い、成功表示前に復号を検証します。鍵は保存しません。入力が変わると古い結果を消去し、ランダムなダイジェストや Base64 を暗号処理の結果として表示してはいけません。
|