@h-ai/crypto 1.0.0-beta.2 → 1.0.0-beta.21
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 +87 -44
- package/README.ja.md +85 -42
- package/README.md +86 -43
- package/dist/index.d.ts +25 -6
- package/dist/index.js +221 -63
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
package/README.en.md
CHANGED
|
@@ -4,28 +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
|
-
|
|
9
7
|
A cryptography module for asymmetric encryption, hashing, symmetric encryption, and password hashing.
|
|
10
8
|
|
|
11
|
-
##
|
|
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.
|
|
9
|
+
## Quick Start
|
|
27
10
|
|
|
28
|
-
|
|
11
|
+
Concurrent `close()` invalidates an unfinished `init()`, so late initialization cannot reopen the module. Wait for that initialization to settle before initializing again.
|
|
29
12
|
|
|
30
13
|
```ts
|
|
31
14
|
import { crypto } from '@h-ai/crypto'
|
|
@@ -36,7 +19,55 @@ if (!initialized.success)
|
|
|
36
19
|
throw new Error(initialized.error.message)
|
|
37
20
|
```
|
|
38
21
|
|
|
39
|
-
|
|
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)
|
|
40
71
|
|
|
41
72
|
```ts
|
|
42
73
|
// 生成密钥对
|
|
@@ -67,7 +98,7 @@ crypto.asymmetric.isValidPublicKey(publicKey) // true
|
|
|
67
98
|
crypto.asymmetric.isValidPrivateKey(privateKey) // true
|
|
68
99
|
```
|
|
69
100
|
|
|
70
|
-
|
|
101
|
+
### Hashing (Hash / HMAC / verification)
|
|
71
102
|
|
|
72
103
|
```ts
|
|
73
104
|
// 字符串哈希
|
|
@@ -91,7 +122,7 @@ if (hash.success) {
|
|
|
91
122
|
}
|
|
92
123
|
```
|
|
93
124
|
|
|
94
|
-
|
|
125
|
+
### Symmetric encryption (default CBC / structured payload)
|
|
95
126
|
|
|
96
127
|
```ts
|
|
97
128
|
// 生成随机密钥和 IV
|
|
@@ -132,7 +163,7 @@ crypto.symmetric.isValidKey(key) // true
|
|
|
132
163
|
crypto.symmetric.isValidIV(iv) // true
|
|
133
164
|
```
|
|
134
165
|
|
|
135
|
-
|
|
166
|
+
### Password hashing (salted iterations)
|
|
136
167
|
|
|
137
168
|
```ts
|
|
138
169
|
// 哈希密码(输出格式: $hai$<iterations>$<salt>$<hash>)
|
|
@@ -154,7 +185,9 @@ if (hashed.success) {
|
|
|
154
185
|
}
|
|
155
186
|
```
|
|
156
187
|
|
|
157
|
-
|
|
188
|
+
### Transport encryption (crypto.transport)
|
|
189
|
+
|
|
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.
|
|
158
191
|
|
|
159
192
|
See the [Transport v2 wire protocol](TRANSPORT-PROTOCOL.md) for canonical encoding, verification order and replay persistence.
|
|
160
193
|
|
|
@@ -167,7 +200,7 @@ For typical applications, use the higher-level integrations:
|
|
|
167
200
|
|
|
168
201
|
Call `crypto.transport.createServer({ serverKeyRing, keyStore })` / `createClient()` directly only for custom runtimes, tests, or manually integrated HTTP key exchange endpoints.
|
|
169
202
|
|
|
170
|
-
|
|
203
|
+
#### Workflow
|
|
171
204
|
|
|
172
205
|
1. Call `await crypto.init()` to initialize `crypto.asymmetric` and `crypto.symmetric`.
|
|
173
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.
|
|
@@ -176,7 +209,7 @@ Call `crypto.transport.createServer({ serverKeyRing, keyStore })` / `createClien
|
|
|
176
209
|
5. Every request sends an authenticated envelope `{ context, direction, status, encryptedKey, ciphertext, iv, tag, signature }`, including empty POST and GET/HEAD.
|
|
177
210
|
6. Custom adapters first await `authenticateTransportRequest(manager, request)`. Only successful `data.plaintext` reaches business code; retain `data.context`.
|
|
178
211
|
7. Encrypt responses with `await manager.encryptResponse(context, data, response.status)`; clients verify context, status and authenticity before returning plaintext.
|
|
179
|
-
8. Clear
|
|
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.
|
|
180
213
|
|
|
181
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.
|
|
182
215
|
|
|
@@ -190,7 +223,7 @@ Registration validates the encoding and SM2 curve point before writing. Only 128
|
|
|
190
223
|
|
|
191
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.
|
|
192
225
|
|
|
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.
|
|
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.
|
|
194
227
|
|
|
195
228
|
```ts
|
|
196
229
|
const registry = createReldbTransportKeyStore({ reldb, ttlSeconds: 3600, maxClients: 10000 })
|
|
@@ -246,17 +279,17 @@ const serverKeyRing = ring.data
|
|
|
246
279
|
const server = crypto.transport.createServer({ serverKeyRing, keyStore })
|
|
247
280
|
```
|
|
248
281
|
|
|
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 generates separate key rings in
|
|
282
|
+
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 generates separate key rings in Git-ignored `.env.dev.local/.env.test.local/.env.prod.local`. After cloning, distribute each environment’s ring through protected files or a Secret Manager. Runtime does not generate keys.
|
|
250
283
|
|
|
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.
|
|
284
|
+
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. By default, requests expire after five minutes and client timestamps may be up to 30 seconds ahead. Set `maxRequestAgeSeconds` (1–600) and `maxFutureSkewSeconds` (0–300) on the server to adjust these limits in whole seconds. The shared registry atomically consumes (clientId, requestId) and retains the nonce until issuedAt plus 11 minutes, covering the longest configurable window and server clock differences. 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
285
|
|
|
253
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.
|
|
254
287
|
|
|
255
|
-
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`. [
|
|
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)。
|
|
256
289
|
|
|
257
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.
|
|
258
291
|
|
|
259
|
-
|
|
292
|
+
Serv/Kit return an encrypted HTTP 400 for an out-of-window request after validating request binding, signature and MAC. The client throws `TransportTimeError` (`hai:crypto:071`), displays the server’s UTC time at validation, and asks the user to enable automatic synchronization or set the device time manually before refreshing. `serverTime` is Unix milliseconds. Clients still show synchronization guidance when an older server omits it. `api-client` maps the error to `VALIDATION_FAILED` and preserves `cause.transportCode` and `cause.serverTime`.
|
|
260
293
|
|
|
261
294
|
Custom HTTP adapters check `error instanceof TransportTimeError && error.context`, then encrypt the rejection with `createTransportErrorResponse(error, { manager, context: error.context })`. Never classify an unverified message by its timestamp.
|
|
262
295
|
|
|
@@ -272,33 +305,32 @@ const text = await readTransportBody(request.body, {
|
|
|
272
305
|
const payload: unknown = JSON.parse(text)
|
|
273
306
|
```
|
|
274
307
|
|
|
275
|
-
|
|
308
|
+
### Close the module
|
|
276
309
|
|
|
277
310
|
```ts
|
|
278
311
|
// 使用完毕后关闭,释放内部状态
|
|
279
312
|
await crypto.close()
|
|
280
313
|
```
|
|
281
314
|
|
|
282
|
-
###
|
|
283
|
-
|
|
284
|
-
All operations return `HaiResult<T>`:
|
|
315
|
+
### Security notice
|
|
285
316
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
// result.error.code / result.error.message
|
|
290
|
-
}
|
|
291
|
-
```
|
|
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.
|
|
292
320
|
|
|
293
321
|
### Optional integrations
|
|
294
322
|
|
|
295
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.
|
|
296
324
|
|
|
297
|
-
##
|
|
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
|
|
298
330
|
|
|
299
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)。
|
|
300
332
|
|
|
301
|
-
|
|
333
|
+
### Module requirements and explicit limits
|
|
302
334
|
|
|
303
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.
|
|
304
336
|
|
|
@@ -309,7 +341,18 @@ const limits = resolveTransportBodyLimits(24 * 1024 * 1024, { requiredPlaintextB
|
|
|
309
341
|
// plaintextBytes: 25165824; encryptedPayloadBytes: 50364416
|
|
310
342
|
```
|
|
311
343
|
|
|
312
|
-
##
|
|
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
|
|
313
356
|
|
|
314
357
|
```bash
|
|
315
358
|
pnpm --filter @h-ai/crypto test
|
package/README.ja.md
CHANGED
|
@@ -4,28 +4,11 @@
|
|
|
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
|
-
### セキュリティに関する注意
|
|
9
|
+
## クイックスタート
|
|
23
10
|
|
|
24
|
-
|
|
25
|
-
- **SM4 にはランダムな鍵を使用してください**:`crypto.symmetric.generateKey()` で鍵を生成します。このモジュールはパスワードから暗号鍵を導出する KDF を提供しません。パスワードの保存にはサーバー専用の `crypto.password.hash(password)` を使用し、パスワードやそのハッシュを直接 SM4 の鍵にしないでください。
|
|
26
|
-
- **IV は毎回一意にしてください**:同じ鍵で IV を再利用してはいけません。既定の CBC または `encryptWithIV()` による毎回の自動生成を推奨します。IV は暗号文と一緒に送信・保存します(公開可能で、秘密にする必要はありません)。
|
|
27
|
-
|
|
28
|
-
### クイックスタート
|
|
11
|
+
並行する `close()` は未完了の `init()` を無効にし、遅い初期化がモジュールを再開しないようにします。その初期化の完了を待ってから再初期化できます。
|
|
29
12
|
|
|
30
13
|
```ts
|
|
31
14
|
import { crypto } from '@h-ai/crypto'
|
|
@@ -36,7 +19,55 @@ if (!initialized.success)
|
|
|
36
19
|
throw new Error(initialized.error.message)
|
|
37
20
|
```
|
|
38
21
|
|
|
39
|
-
|
|
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
|
+
### 非対称暗号(暗号化・復号 / 署名・検証)
|
|
40
71
|
|
|
41
72
|
```ts
|
|
42
73
|
// 生成密钥对
|
|
@@ -67,7 +98,7 @@ crypto.asymmetric.isValidPublicKey(publicKey) // true
|
|
|
67
98
|
crypto.asymmetric.isValidPrivateKey(privateKey) // true
|
|
68
99
|
```
|
|
69
100
|
|
|
70
|
-
|
|
101
|
+
### ハッシュ(Hash / HMAC / 検証)
|
|
71
102
|
|
|
72
103
|
```ts
|
|
73
104
|
// 字符串哈希
|
|
@@ -91,7 +122,7 @@ if (hash.success) {
|
|
|
91
122
|
}
|
|
92
123
|
```
|
|
93
124
|
|
|
94
|
-
|
|
125
|
+
### 対称暗号(既定の CBC / 構造化暗号文)
|
|
95
126
|
|
|
96
127
|
```ts
|
|
97
128
|
// 生成随机密钥和 IV
|
|
@@ -132,7 +163,7 @@ crypto.symmetric.isValidKey(key) // true
|
|
|
132
163
|
crypto.symmetric.isValidIV(iv) // true
|
|
133
164
|
```
|
|
134
165
|
|
|
135
|
-
|
|
166
|
+
### パスワードハッシュ(ソルト付き反復)
|
|
136
167
|
|
|
137
168
|
```ts
|
|
138
169
|
// 哈希密码(输出格式: $hai$<iterations>$<salt>$<hash>)
|
|
@@ -154,7 +185,7 @@ if (hashed.success) {
|
|
|
154
185
|
}
|
|
155
186
|
```
|
|
156
187
|
|
|
157
|
-
|
|
188
|
+
### 通信暗号化(crypto.transport)
|
|
158
189
|
|
|
159
190
|
正規化、検証順序、nonce 保存は [Transport v2 プロトコル](TRANSPORT-PROTOCOL.md) を参照してください。
|
|
160
191
|
|
|
@@ -167,7 +198,7 @@ if (hashed.success) {
|
|
|
167
198
|
|
|
168
199
|
`crypto.transport.createServer({ serverKeyRing, keyStore })` / `createClient()` の直接呼び出しは、独自ランタイム、テスト、HTTP 鍵交換 endpoint を自分で接続する場合に限って推奨します。
|
|
169
200
|
|
|
170
|
-
|
|
201
|
+
#### 使用手順
|
|
171
202
|
|
|
172
203
|
1. `await crypto.init()` を呼び、`crypto.asymmetric` と `crypto.symmetric` を初期化します。
|
|
173
204
|
2. サーバーで `crypto.transport.createServer({ serverKeyRing, keyStore })` を呼び、`manager` を作成します。サーバー鍵ペアとクライアント公開鍵を管理します。複数ノードでは共有 `keyStore` を注入できます。
|
|
@@ -176,7 +207,7 @@ if (hashed.success) {
|
|
|
176
207
|
5. 空の POST と GET/HEAD を含め、すべての要求が認証封装 `{ context, direction, status, encryptedKey, ciphertext, iv, tag, signature }` を送ります。
|
|
177
208
|
6. 独自アダプターは先に `await authenticateTransportRequest(manager, request)` を実行し、成功時のみ `data.plaintext` を業務へ渡し、`data.context` を保持します。
|
|
178
209
|
7. 応答は `await manager.encryptResponse(context, data, response.status)` で元要求に関連付け、クライアントがコンテキスト・status・完全性を検証してから平文を返します。
|
|
179
|
-
8. `client.destroy()`
|
|
210
|
+
8. `client.destroy()` でローカルのセッションを消去します。サーバーでは `await manager.close()` の後に `await crypto.close()` を呼びます。モジュールを閉じても、独立して作成した管理器の終了や業務セッションの失効は行いません。
|
|
180
211
|
|
|
181
212
|
> サーバーには同じ管理済み `serverKeyRing` と共有 `keyStore`(Redis または共有 DB)を明示的に渡します。クライアント公開鍵の共有だけではノード間で復号できません。暗黙のプロセス鍵・メモリー registry はなく、開発・テストでは `createInMemoryKeyStore(capacity)` を明示します。再起動後は同じ鍵環を読み込み、sticky session は不要です。
|
|
182
213
|
|
|
@@ -190,7 +221,7 @@ if (hashed.success) {
|
|
|
190
221
|
|
|
191
222
|
Redis/DB の `ttlSeconds` を省略すると無期限です。指定時は 1–2147483647 の整数秒とし、有限 TTL を推奨します。Redis はネイティブのキー TTL を使い、DB の `maxClients` 上限は提供しません。メモリー予算を設定し、有効な nonce を追い出さないでください。DB の `maxClients` は正の安全な整数、既定 10000 で、全ノードと永続容量レコードが一致する必要があります。登録はトランザクション内で共有容量行をロックし、件数確認後に挿入します。満杯では登録を拒否し、有効な公開鍵や nonce を削除しません。クライアント削除時も有効な nonce は保持します。
|
|
192
223
|
|
|
193
|
-
DB の期限は DB 時計の epoch ミリ秒で生成・判定します。DB と各ノードの時計をプロトコルの許容範囲内に同期してください。初回テーブル作成失敗後は再試行できます。登録時に 1 バッチ清掃し、`await registry.pruneExpired()` は期限切れ公開鍵と nonce を各 100 件まで削除します。低トラフィック時もアプリで定期実行してください。暗黙のタイマーは作らず、注入された reldb
|
|
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 を消さないでください。
|
|
194
225
|
|
|
195
226
|
```ts
|
|
196
227
|
const registry = createReldbTransportKeyStore({ reldb, ttlSeconds: 3600, maxClients: 10000 })
|
|
@@ -246,17 +277,17 @@ const serverKeyRing = ring.data
|
|
|
246
277
|
const server = crypto.transport.createServer({ serverKeyRing, keyStore })
|
|
247
278
|
```
|
|
248
279
|
|
|
249
|
-
鍵交換は `{ serverKeyId, serverPublicKey, clientId }` を返し、暗号化リクエストは `serverKeyId` を送ります。サーバーは指定した秘密鍵だけを選び、不明・失効 ID は他の鍵を試さず拒否します。応答はクライアント公開鍵で暗号化します。更新時は全ノードに新旧の鍵を先に配布し、続いて `activeKeyId` を切り替えて manager を順次再構築します。移行期間後に古い ID を全ノードから削除します。旧クライアントは `destroy()` 後に再交渉し、非冪等処理を自動再送しません。同じ鍵環を Secret Manager またはサーバー専用の非公開環境から配布し、通常の YAML、PUBLIC_/VITE_、ログ、公開応答には含めないでください。CLI は `.env.dev/.env.test/.env.prod`
|
|
280
|
+
鍵交換は `{ serverKeyId, serverPublicKey, clientId }` を返し、暗号化リクエストは `serverKeyId` を送ります。サーバーは指定した秘密鍵だけを選び、不明・失効 ID は他の鍵を試さず拒否します。応答はクライアント公開鍵で暗号化します。更新時は全ノードに新旧の鍵を先に配布し、続いて `activeKeyId` を切り替えて manager を順次再構築します。移行期間後に古い ID を全ノードから削除します。旧クライアントは `destroy()` 後に再交渉し、非冪等処理を自動再送しません。同じ鍵環を Secret Manager またはサーバー専用の非公開環境から配布し、通常の YAML、PUBLIC_/VITE_、ログ、公開応答には含めないでください。CLI は Git 対象外の `.env.dev.local/.env.test.local/.env.prod.local` に独立した鍵環を作成します。クローン後は保護ファイルまたは Secret Manager で環境ごとに配布します。実行時には生成しません。
|
|
250
281
|
|
|
251
|
-
認証済み v2 のみを受け付けます。独立したランダム SM4-CBC 鍵と HMAC-SM3 鍵を SM2 で包み、送信者署名、MAC の順に検証してから CBC を復号します。要求はクライアント秘密鍵、応答は選択されたサーバー鍵バージョンで署名します。version、direction、serverKeyId、clientId、nonce を兼ねるランダム 128 ビット requestId、issuedAt、HTTP method、pathname と元の search の SM3 pathHash、応答 status
|
|
282
|
+
認証済み 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 秒先まで許容されます。サーバー側の `maxRequestAgeSeconds`(1~600)と `maxFutureSkewSeconds`(0~300)で整数秒単位に調整できます。共有レジストリは (clientId, requestId) をアトミックに消費し、設定可能な最長の有効期間とノード間の時刻差をカバーするため、nonce を issuedAt の 11 分後まで保持します。ストレージ障害時は要求を拒否します。時計同期と Redis の noeviction/永続化により有効な nonce を保持してください。業務の再試行には別の冪等キーが必要です。
|
|
252
283
|
|
|
253
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 の業務認可は別に必要です。
|
|
254
285
|
|
|
255
|
-
`transport.maxPlaintextBytes` でリクエストとレスポンスの JSON 平文容量を共通設定します。単位は UTF-8 バイト、既定値は 1 MiB、有効範囲は 1 KiB~64 MiB の整数です。クライアントは鍵交換で上限を取得し、`resolveTransportBodyLimits()` が暗号文とプロキシ容量(平文の 2 倍 + 32 KiB)を算出します。鍵交換、公開鍵、認証ヘッダー、防リプレイ制約は独立です。超過時は元ストリームを停止して reader を解放します。要求は 413、応答は暗号化された 500 容量エラーとなり、`TransportBodyTooLargeError` / `hai:crypto:070` で識別できます。 [
|
|
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)。
|
|
256
287
|
|
|
257
288
|
通常業務は `transport.maxPlaintextBytes`(auto/省略は 1 MiB)を維持します。Feedback の受理・証拠・添付・ZIP ダウンロードは `routePlaintextBytes` と `feedback.budget.maxTransportBytes` で別途宣言します。`*` は完全な1パス区間のみ一致し、ホストの明示値が優先です。ハンドシェイク、adapter-node、プロキシは経路の最大値を使用します。HTTP アダプターは個別宣言された 1 MiB 超の資料経路を最大2件、通常業務を別枠で16件まで許可し、応答完了・失敗時に解放します。混雑は503 / `hai:crypto:072` を返し、業務書込は自動再送しません。
|
|
258
289
|
|
|
259
|
-
|
|
290
|
+
Serv/Kit は要求の結合、署名、MAC を検証した後、時刻の許容範囲を超えた要求に暗号化した HTTP 400 を返します。クライアントは `TransportTimeError`(`hai:crypto:071`)をスローし、検証時のサーバー UTC 時刻を表示して、自動時刻同期を有効にするか手動で時刻を設定してから再読み込みするよう案内します。`serverTime` は Unix ミリ秒です。旧サーバーが値を返さない場合も時刻調整の案内を表示します。`api-client` は `VALIDATION_FAILED` に変換し、`cause.transportCode` と `cause.serverTime` を保持します。
|
|
260
291
|
|
|
261
292
|
独自 HTTP アダプターでは `error instanceof TransportTimeError && error.context` を確認し、`createTransportErrorResponse(error, { manager, context: error.context })` で拒否応答を暗号化します。未認証の報文のタイムスタンプだけで時刻異常を診断しないでください。
|
|
262
293
|
|
|
@@ -272,33 +303,34 @@ const text = await readTransportBody(request.body, {
|
|
|
272
303
|
const payload: unknown = JSON.parse(text)
|
|
273
304
|
```
|
|
274
305
|
|
|
275
|
-
|
|
306
|
+
### モジュールの終了
|
|
276
307
|
|
|
277
308
|
```ts
|
|
278
309
|
// 使用完毕后关闭,释放内部状态
|
|
279
310
|
await crypto.close()
|
|
280
311
|
```
|
|
281
312
|
|
|
282
|
-
###
|
|
313
|
+
### セキュリティに関する注意
|
|
283
314
|
|
|
284
|
-
|
|
315
|
+
伝送クライアントの `destroy()` は進行中のハンドシェイクを無効化して中断し、遅延結果による鍵の復活や次回ハンドシェイクの上書きを防ぎます。サーバー registry は自身の期限で回収します。
|
|
285
316
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
// result.error.code / result.error.message
|
|
290
|
-
}
|
|
291
|
-
```
|
|
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 は暗号文と一緒に送信・保存します(公開可能で、秘密にする必要はありません)。
|
|
292
320
|
|
|
293
321
|
### 任意の連携
|
|
294
322
|
|
|
295
323
|
管理画面のオンライン暗号化デモは Crypto の SM3/SM4 API を直接呼びます。SM4 はランダムな一時鍵と IV を使い、成功表示前に復号を検証します。鍵は保存しません。入力が変わると古い結果を消去し、ランダムなダイジェストや Base64 を暗号処理の結果として表示してはいけません。
|
|
296
324
|
|
|
297
|
-
##
|
|
325
|
+
## 設定
|
|
326
|
+
|
|
327
|
+
crypto.init() に設定オブジェクトは渡しません。アルゴリズムのオプションは各呼出しに渡し、HTTP 転送は crypto.transport.createServer/createClient で設定します。サーバー鍵リングと共有 registry は明示的に注入し、秘密鍵をブラウザーや公開 YAML に含めません。
|
|
328
|
+
|
|
329
|
+
### 本番の共有状態
|
|
298
330
|
|
|
299
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)。
|
|
300
332
|
|
|
301
|
-
|
|
333
|
+
### モジュール要件と明示的な上限
|
|
302
334
|
|
|
303
335
|
`resolveTransportBodyLimits(limit?, { requiredPlaintextBytes? })` は limit 省略時だけ有効なモジュールの必要量から計算し、要件がなければ 1 MiB です。明示上限が必要量未満なら RangeError とし、勝手に拡張・縮小しません。範囲は 1 KiB~64 MiB、暗号文は平文の2倍+32 KiBです。Serv/Kit の `auto` は実行時に undefined へ正規化します。
|
|
304
336
|
|
|
@@ -309,6 +341,17 @@ const limits = resolveTransportBodyLimits(24 * 1024 * 1024, { requiredPlaintextB
|
|
|
309
341
|
// plaintextBytes: 25165824; encryptedPayloadBytes: 50364416
|
|
310
342
|
```
|
|
311
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
|
+
|
|
312
355
|
## 開発とテスト
|
|
313
356
|
|
|
314
357
|
```bash
|