@h-ai/crypto 0.1.0-alpha.56 → 0.1.0-alpha.58
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 +248 -0
- package/README.ja.md +248 -0
- package/README.md +4 -0
- package/package.json +9 -9
package/README.en.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# @h-ai/crypto
|
|
2
|
+
|
|
3
|
+
[简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
A cryptography module for asymmetric encryption, hashing, symmetric encryption, and password hashing.
|
|
8
|
+
|
|
9
|
+
## Capabilities
|
|
10
|
+
|
|
11
|
+
- Asymmetric cryptography (key generation, encryption/decryption, signing/verification)
|
|
12
|
+
- Hashing (hash, HMAC, verification)
|
|
13
|
+
- Symmetric encryption (ECB/CBC modes)
|
|
14
|
+
- Password hashing (salted iterative hashes)
|
|
15
|
+
- Transport encryption (`crypto.transport`: server manager + client `encryptedFetch`)
|
|
16
|
+
- Shared server/browser support (Node.js / browser)
|
|
17
|
+
|
|
18
|
+
## Security notice
|
|
19
|
+
|
|
20
|
+
- **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
|
+
- **SM4 `deriveKey(password, salt)` is not a KDF**: it performs a single SM3 hash and offers no resistance to password cracking. **Never use it for password storage**. Use `crypto.password.hash(password)` for password hashing and PBKDF2 / scrypt / Argon2 for password-based key derivation.
|
|
22
|
+
- **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
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { crypto } from '@h-ai/crypto'
|
|
28
|
+
|
|
29
|
+
// 初始化(使用前必须调用)
|
|
30
|
+
await crypto.init()
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Asymmetric cryptography (encryption / signing)
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// 生成密钥对
|
|
37
|
+
const keyPair = crypto.asymmetric.generateKeyPair()
|
|
38
|
+
if (!keyPair.success)
|
|
39
|
+
throw new Error(keyPair.error.message)
|
|
40
|
+
const { publicKey, privateKey } = keyPair.data
|
|
41
|
+
|
|
42
|
+
// 加密 / 解密
|
|
43
|
+
const encrypted = crypto.asymmetric.encrypt('Hello', publicKey)
|
|
44
|
+
if (encrypted.success) {
|
|
45
|
+
const decrypted = crypto.asymmetric.decrypt(encrypted.data, privateKey)
|
|
46
|
+
// decrypted.data === 'Hello'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 输出 base64 格式密文
|
|
50
|
+
const b64 = crypto.asymmetric.encrypt('Hello', publicKey, { outputFormat: 'base64' })
|
|
51
|
+
|
|
52
|
+
// 签名 / 验签
|
|
53
|
+
const sig = crypto.asymmetric.sign('important data', privateKey)
|
|
54
|
+
if (sig.success) {
|
|
55
|
+
const valid = crypto.asymmetric.verify('important data', sig.data, publicKey)
|
|
56
|
+
// valid.data === true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 校验密钥格式
|
|
60
|
+
crypto.asymmetric.isValidPublicKey(publicKey) // true
|
|
61
|
+
crypto.asymmetric.isValidPrivateKey(privateKey) // true
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Hashing (Hash / HMAC / verification)
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// 字符串哈希
|
|
68
|
+
const hash = crypto.hash.hash('Hello!')
|
|
69
|
+
// hash.data → 64 字符十六进制哈希值
|
|
70
|
+
|
|
71
|
+
// Uint8Array 输入
|
|
72
|
+
const buf = new TextEncoder().encode('Hello!')
|
|
73
|
+
const hashBuf = crypto.hash.hash(buf)
|
|
74
|
+
|
|
75
|
+
// 十六进制编码输入
|
|
76
|
+
const hashHex = crypto.hash.hash('48656c6c6f21', { inputEncoding: 'hex' })
|
|
77
|
+
|
|
78
|
+
// HMAC
|
|
79
|
+
const hmac = crypto.hash.hmac('message', 'secret-key')
|
|
80
|
+
|
|
81
|
+
// 验证哈希是否匹配
|
|
82
|
+
if (hash.success) {
|
|
83
|
+
const matched = crypto.hash.verify('Hello!', hash.data)
|
|
84
|
+
// matched.data === true
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Symmetric encryption (default CBC / structured payload)
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
// 生成随机密钥和 IV
|
|
92
|
+
const key = crypto.symmetric.generateKey()
|
|
93
|
+
const iv = crypto.symmetric.generateIV()
|
|
94
|
+
|
|
95
|
+
// 默认:CBC + 自动随机 IV,返回结构化密文
|
|
96
|
+
const safeEnc = crypto.symmetric.encrypt('data', key)
|
|
97
|
+
if (safeEnc.success) {
|
|
98
|
+
// safeEnc.data: { mode: 'cbc', ciphertext, iv, encoding: 'hex' }
|
|
99
|
+
const safeDec = crypto.symmetric.decrypt(safeEnc.data, key)
|
|
100
|
+
// safeDec.data === 'data'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// CBC 指定 IV(仍然返回结构化密文)
|
|
104
|
+
const cbcEnc = crypto.symmetric.encrypt('data', key, { mode: 'cbc', iv })
|
|
105
|
+
if (cbcEnc.success) {
|
|
106
|
+
const cbcDec = crypto.symmetric.decrypt(cbcEnc.data, key)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 结构化结果:自动生成 IV 的 CBC 加解密
|
|
110
|
+
const withIV = crypto.symmetric.encryptWithIV('data', key)
|
|
111
|
+
if (withIV.success) {
|
|
112
|
+
const dec = crypto.symmetric.decryptWithIV(withIV.data.ciphertext, key, withIV.data.iv)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ECB 模式(❌ 不安全;只有底层协议明确要求时才显式指定)
|
|
116
|
+
const ecbEnc = crypto.symmetric.encrypt('data', key, { mode: 'ecb' })
|
|
117
|
+
if (ecbEnc.success) {
|
|
118
|
+
const ecbDec = crypto.symmetric.decrypt(ecbEnc.data, key)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 输出 base64 格式(ciphertext 字段为 base64,encoding 字段会标明)
|
|
122
|
+
const b64Enc = crypto.symmetric.encrypt('data', key, { outputFormat: 'base64' })
|
|
123
|
+
|
|
124
|
+
// ⚠️ 已弃用:从密码派生密钥(仅为单次 SM3 哈希,不是 KDF,禁止用于密码存储场景)
|
|
125
|
+
// 密码散列请用 crypto.password.hash();如需从密码派生密钥,请在应用层自行实现 PBKDF2 / scrypt / Argon2
|
|
126
|
+
const derivedKey = crypto.symmetric.deriveKey('my-password', 'random-salt')
|
|
127
|
+
|
|
128
|
+
// 校验密钥/IV 格式
|
|
129
|
+
crypto.symmetric.isValidKey(key) // true
|
|
130
|
+
crypto.symmetric.isValidIV(iv) // true
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Password hashing (salted iterations)
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
// 哈希密码(输出格式: $hai$<iterations>$<salt>$<hash>)
|
|
137
|
+
const hashed = crypto.password.hash('myPassword123')
|
|
138
|
+
|
|
139
|
+
// 自定义盐值长度和迭代次数
|
|
140
|
+
const custom = crypto.password.hash('myPassword123', {
|
|
141
|
+
saltLength: 32,
|
|
142
|
+
iterations: 20000,
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// 验证密码
|
|
146
|
+
if (hashed.success) {
|
|
147
|
+
const ok = crypto.password.verify('myPassword123', hashed.data)
|
|
148
|
+
// ok.data === true
|
|
149
|
+
|
|
150
|
+
const wrong = crypto.password.verify('wrongPassword', hashed.data)
|
|
151
|
+
// wrong.data === false
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Transport encryption (crypto.transport)
|
|
156
|
+
|
|
157
|
+
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
|
+
|
|
159
|
+
For typical applications, use the higher-level integrations:
|
|
160
|
+
|
|
161
|
+
- Server: `serv.createApp({ transport: { crypto } })` or `kit.createHandle({ crypto: { crypto, transport: true } })`.
|
|
162
|
+
- Client: `apiClient.init({ transport: { crypto } })` or `kit.client.create({ transport: { crypto } })`.
|
|
163
|
+
|
|
164
|
+
Call `crypto.transport.createServer()` / `createClient()` directly only for custom runtimes, tests, or manually integrated HTTP key exchange endpoints.
|
|
165
|
+
|
|
166
|
+
#### Workflow
|
|
167
|
+
|
|
168
|
+
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 }`.
|
|
171
|
+
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. After key exchange, requests include `X-Client-Id`. A request body is wrapped as `{ encryptedKey, ciphertext, iv }` with `X-Encrypted: true`. Bodyless requests include only `X-Client-Id` and do not create an encrypted body.
|
|
173
|
+
6. The server looks up the client public key by `clientId`, decrypts the body with `manager.decryptRequest()`, then runs the business logic.
|
|
174
|
+
7. Before returning JSON, encrypt it with `manager.encryptResponse(clientId, data)` and set `X-Encrypted: true`. The client decrypts the response automatically.
|
|
175
|
+
8. Clear a client session with `client.destroy()`. Release server resources with `manager.close()` or module-level `await crypto.close()`.
|
|
176
|
+
|
|
177
|
+
> The default `keyStore` is an in-process FIFO store; exceeding `maxClients` evicts the earliest registered client. Multi-node deployments need sticky sessions or a shared `TransportKeyStore` implementation for client public keys.
|
|
178
|
+
|
|
179
|
+
For shared storage, reuse the provider factories exported from the `@h-ai/crypto` root entry:
|
|
180
|
+
|
|
181
|
+
- `createInMemoryKeyStore(maxClients)`: explicitly create the default memory store for tests or custom capacity.
|
|
182
|
+
- `createRedisTransportKeyStore({ cache, ttlSeconds? })`: reuse `@h-ai/cache`, typically with Redis for sharing across nodes.
|
|
183
|
+
- `createReldbTransportKeyStore({ reldb, ttlSeconds? })`: reuse `@h-ai/reldb`, automatically creating `hai_crypto_transport_client_keys`.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
// 服务端:通常由 serv.createApp({ transport: { crypto } }) 或 kit.createHandle({ crypto }) 内部调用
|
|
187
|
+
const server = crypto.transport.createServer({ maxClients: 10000 })
|
|
188
|
+
if (!server.success)
|
|
189
|
+
throw new Error(server.error.message)
|
|
190
|
+
|
|
191
|
+
// 客户端:通常由 apiClient.init({ transport: { crypto } }) 或 kit.client.create({ transport }) 内部调用
|
|
192
|
+
const client = crypto.transport.createClient({
|
|
193
|
+
keyExchangeUrl: 'https://api.example.com/api/v1/_hai/key-exchange',
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const response = await client.encryptedFetch('https://api.example.com/api/v1/echo', {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
body: JSON.stringify({ hello: 'world' }),
|
|
199
|
+
})
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
import { cache } from '@h-ai/cache'
|
|
204
|
+
import { createRedisTransportKeyStore, crypto } from '@h-ai/crypto'
|
|
205
|
+
|
|
206
|
+
await crypto.init()
|
|
207
|
+
await cache.init({ type: 'redis', host: '127.0.0.1', port: 6379 })
|
|
208
|
+
|
|
209
|
+
const server = crypto.transport.createServer({
|
|
210
|
+
keyStore: createRedisTransportKeyStore({ cache, ttlSeconds: 3600 }),
|
|
211
|
+
})
|
|
212
|
+
if (!server.success)
|
|
213
|
+
throw new Error(server.error.message)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`encryptedFetch()` follows native `fetch` semantics: it rejects on network, key exchange, request encryption, or response decryption failures. Catch these at the call site using the application's fetch error strategy.
|
|
217
|
+
|
|
218
|
+
Access protocol constants through `crypto.transport.protocol` (or `TRANSPORT_PROTOCOL`): `X-Client-Id`, `X-Encrypted`, and the default `/_hai/key-exchange`.
|
|
219
|
+
|
|
220
|
+
### Close the module
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
// 使用完毕后关闭,释放内部状态
|
|
224
|
+
await crypto.close()
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Error handling
|
|
228
|
+
|
|
229
|
+
All operations return `HaiResult<T>`:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
const result = crypto.asymmetric.encrypt('data', publicKey)
|
|
233
|
+
if (!result.success) {
|
|
234
|
+
// result.error.code / result.error.message
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Tests
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
pnpm --filter @h-ai/crypto test
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## License
|
|
245
|
+
|
|
246
|
+
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
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# @h-ai/crypto
|
|
2
|
+
|
|
3
|
+
[简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
非対称暗号、ハッシュ、対称暗号、パスワードハッシュを提供する暗号モジュールです。
|
|
8
|
+
|
|
9
|
+
## 主な機能
|
|
10
|
+
|
|
11
|
+
- 非対称暗号(鍵生成、暗号化・復号、署名・検証)
|
|
12
|
+
- ハッシュ(Hash、HMAC、検証)
|
|
13
|
+
- 対称暗号(ECB/CBC モード)
|
|
14
|
+
- パスワードハッシュ(ソルト付き反復ハッシュ)
|
|
15
|
+
- 通信暗号化(`crypto.transport`:サーバー管理機能 + クライアントの `encryptedFetch`)
|
|
16
|
+
- サーバー・ブラウザー共通(Node.js / ブラウザー)
|
|
17
|
+
|
|
18
|
+
## セキュリティに関する注意
|
|
19
|
+
|
|
20
|
+
- **SM4 の既定の暗号化結果は構造化された暗号文**:`crypto.symmetric.encrypt(data, key)` は既定で CBC + ランダム IV を使用し、`{ mode, ciphertext, iv, encoding }` を返します。復号時はこのオブジェクトを `crypto.symmetric.decrypt(payload, key)` に渡します。
|
|
21
|
+
- **SM4 の `deriveKey(password, salt)` は KDF ではありません**:SM3 を 1 回実行するだけで、パスワードの総当たり攻撃への耐性はありません。**パスワード保存には使用禁止**です。パスワードハッシュには `crypto.password.hash(password)`、パスワードからの鍵導出には PBKDF2 / scrypt / Argon2 を使用してください。
|
|
22
|
+
- **IV は毎回一意にしてください**:同じ鍵で IV を再利用してはいけません。既定の CBC または `encryptWithIV()` による毎回の自動生成を推奨します。IV は暗号文と一緒に送信・保存します(公開可能で、秘密にする必要はありません)。
|
|
23
|
+
|
|
24
|
+
## クイックスタート
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { crypto } from '@h-ai/crypto'
|
|
28
|
+
|
|
29
|
+
// 初始化(使用前必须调用)
|
|
30
|
+
await crypto.init()
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 非対称暗号(暗号化・復号 / 署名・検証)
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// 生成密钥对
|
|
37
|
+
const keyPair = crypto.asymmetric.generateKeyPair()
|
|
38
|
+
if (!keyPair.success)
|
|
39
|
+
throw new Error(keyPair.error.message)
|
|
40
|
+
const { publicKey, privateKey } = keyPair.data
|
|
41
|
+
|
|
42
|
+
// 加密 / 解密
|
|
43
|
+
const encrypted = crypto.asymmetric.encrypt('Hello', publicKey)
|
|
44
|
+
if (encrypted.success) {
|
|
45
|
+
const decrypted = crypto.asymmetric.decrypt(encrypted.data, privateKey)
|
|
46
|
+
// decrypted.data === 'Hello'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 输出 base64 格式密文
|
|
50
|
+
const b64 = crypto.asymmetric.encrypt('Hello', publicKey, { outputFormat: 'base64' })
|
|
51
|
+
|
|
52
|
+
// 签名 / 验签
|
|
53
|
+
const sig = crypto.asymmetric.sign('important data', privateKey)
|
|
54
|
+
if (sig.success) {
|
|
55
|
+
const valid = crypto.asymmetric.verify('important data', sig.data, publicKey)
|
|
56
|
+
// valid.data === true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 校验密钥格式
|
|
60
|
+
crypto.asymmetric.isValidPublicKey(publicKey) // true
|
|
61
|
+
crypto.asymmetric.isValidPrivateKey(privateKey) // true
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### ハッシュ(Hash / HMAC / 検証)
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// 字符串哈希
|
|
68
|
+
const hash = crypto.hash.hash('Hello!')
|
|
69
|
+
// hash.data → 64 字符十六进制哈希值
|
|
70
|
+
|
|
71
|
+
// Uint8Array 输入
|
|
72
|
+
const buf = new TextEncoder().encode('Hello!')
|
|
73
|
+
const hashBuf = crypto.hash.hash(buf)
|
|
74
|
+
|
|
75
|
+
// 十六进制编码输入
|
|
76
|
+
const hashHex = crypto.hash.hash('48656c6c6f21', { inputEncoding: 'hex' })
|
|
77
|
+
|
|
78
|
+
// HMAC
|
|
79
|
+
const hmac = crypto.hash.hmac('message', 'secret-key')
|
|
80
|
+
|
|
81
|
+
// 验证哈希是否匹配
|
|
82
|
+
if (hash.success) {
|
|
83
|
+
const matched = crypto.hash.verify('Hello!', hash.data)
|
|
84
|
+
// matched.data === true
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 対称暗号(既定の CBC / 構造化暗号文)
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
// 生成随机密钥和 IV
|
|
92
|
+
const key = crypto.symmetric.generateKey()
|
|
93
|
+
const iv = crypto.symmetric.generateIV()
|
|
94
|
+
|
|
95
|
+
// 默认:CBC + 自动随机 IV,返回结构化密文
|
|
96
|
+
const safeEnc = crypto.symmetric.encrypt('data', key)
|
|
97
|
+
if (safeEnc.success) {
|
|
98
|
+
// safeEnc.data: { mode: 'cbc', ciphertext, iv, encoding: 'hex' }
|
|
99
|
+
const safeDec = crypto.symmetric.decrypt(safeEnc.data, key)
|
|
100
|
+
// safeDec.data === 'data'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// CBC 指定 IV(仍然返回结构化密文)
|
|
104
|
+
const cbcEnc = crypto.symmetric.encrypt('data', key, { mode: 'cbc', iv })
|
|
105
|
+
if (cbcEnc.success) {
|
|
106
|
+
const cbcDec = crypto.symmetric.decrypt(cbcEnc.data, key)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 结构化结果:自动生成 IV 的 CBC 加解密
|
|
110
|
+
const withIV = crypto.symmetric.encryptWithIV('data', key)
|
|
111
|
+
if (withIV.success) {
|
|
112
|
+
const dec = crypto.symmetric.decryptWithIV(withIV.data.ciphertext, key, withIV.data.iv)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ECB 模式(❌ 不安全;只有底层协议明确要求时才显式指定)
|
|
116
|
+
const ecbEnc = crypto.symmetric.encrypt('data', key, { mode: 'ecb' })
|
|
117
|
+
if (ecbEnc.success) {
|
|
118
|
+
const ecbDec = crypto.symmetric.decrypt(ecbEnc.data, key)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 输出 base64 格式(ciphertext 字段为 base64,encoding 字段会标明)
|
|
122
|
+
const b64Enc = crypto.symmetric.encrypt('data', key, { outputFormat: 'base64' })
|
|
123
|
+
|
|
124
|
+
// ⚠️ 已弃用:从密码派生密钥(仅为单次 SM3 哈希,不是 KDF,禁止用于密码存储场景)
|
|
125
|
+
// 密码散列请用 crypto.password.hash();如需从密码派生密钥,请在应用层自行实现 PBKDF2 / scrypt / Argon2
|
|
126
|
+
const derivedKey = crypto.symmetric.deriveKey('my-password', 'random-salt')
|
|
127
|
+
|
|
128
|
+
// 校验密钥/IV 格式
|
|
129
|
+
crypto.symmetric.isValidKey(key) // true
|
|
130
|
+
crypto.symmetric.isValidIV(iv) // true
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### パスワードハッシュ(ソルト付き反復)
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
// 哈希密码(输出格式: $hai$<iterations>$<salt>$<hash>)
|
|
137
|
+
const hashed = crypto.password.hash('myPassword123')
|
|
138
|
+
|
|
139
|
+
// 自定义盐值长度和迭代次数
|
|
140
|
+
const custom = crypto.password.hash('myPassword123', {
|
|
141
|
+
saltLength: 32,
|
|
142
|
+
iterations: 20000,
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// 验证密码
|
|
146
|
+
if (hashed.success) {
|
|
147
|
+
const ok = crypto.password.verify('myPassword123', hashed.data)
|
|
148
|
+
// ok.data === true
|
|
149
|
+
|
|
150
|
+
const wrong = crypto.password.verify('wrongPassword', hashed.data)
|
|
151
|
+
// wrong.data === false
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 通信暗号化(crypto.transport)
|
|
156
|
+
|
|
157
|
+
通信暗号化は `crypto.transport` 名前空間で提供し、`@h-ai/serv`、`@h-ai/kit`、`@h-ai/api-client` で同じプロトコル定数とペイロード形式を再利用します。
|
|
158
|
+
|
|
159
|
+
通常のアプリケーションでは、上位モジュールの統合機能を優先してください:
|
|
160
|
+
|
|
161
|
+
- サーバー:`serv.createApp({ transport: { crypto } })` または `kit.createHandle({ crypto: { crypto, transport: true } })`。
|
|
162
|
+
- クライアント:`apiClient.init({ transport: { crypto } })` または `kit.client.create({ transport: { crypto } })`。
|
|
163
|
+
|
|
164
|
+
`crypto.transport.createServer()` / `createClient()` の直接呼び出しは、独自ランタイム、テスト、HTTP 鍵交換 endpoint を自分で接続する場合に限って推奨します。
|
|
165
|
+
|
|
166
|
+
#### 使用手順
|
|
167
|
+
|
|
168
|
+
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 }` を返します。
|
|
171
|
+
4. クライアントで `crypto.transport.createClient({ keyExchangeUrl })` によりセッションを作成します。最初の `client.init()` または `client.encryptedFetch()` で鍵交換が自動実行されます。
|
|
172
|
+
5. 鍵交換後、リクエストに `X-Client-Id` を付与します。body がある場合は `{ encryptedKey, ciphertext, iv }` に変換し、`X-Encrypted: true` を付けます。body のないリクエストでは `X-Client-Id` のみを付与し、暗号文 body は作成しません。
|
|
173
|
+
6. サーバーは `clientId` でクライアント公開鍵を取得し、`manager.decryptRequest()` でリクエスト本文を復号してから業務処理を実行します。
|
|
174
|
+
7. JSON 応答を返す前に `manager.encryptResponse(clientId, data)` で暗号化し、`X-Encrypted: true` を付けます。クライアントは応答を自動的に復号します。
|
|
175
|
+
8. `client.destroy()` でクライアントセッションを消去します。サーバー側のリソースは `manager.close()` またはモジュール単位の `await crypto.close()` で解放します。
|
|
176
|
+
|
|
177
|
+
> 既定の `keyStore` はプロセス内の FIFO メモリーストアです。`maxClients` を超えると最初に登録されたクライアントを削除します。複数ノードでは sticky session、またはクライアント公開鍵を共有する `TransportKeyStore` 実装が必要です。
|
|
178
|
+
|
|
179
|
+
共有ストレージには、`@h-ai/crypto` のルート入口からエクスポートされる Provider ファクトリーを再利用できます:
|
|
180
|
+
|
|
181
|
+
- `createInMemoryKeyStore(maxClients)`:テストや容量指定用に、既定のメモリーストアを明示的に作成。
|
|
182
|
+
- `createRedisTransportKeyStore({ cache, ttlSeconds? })`:`@h-ai/cache` を再利用。通常は Redis Provider と組み合わせ、複数ノードで共有。
|
|
183
|
+
- `createReldbTransportKeyStore({ reldb, ttlSeconds? })`:`@h-ai/reldb` を再利用し、`hai_crypto_transport_client_keys` テーブルを自動作成。
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
// 服务端:通常由 serv.createApp({ transport: { crypto } }) 或 kit.createHandle({ crypto }) 内部调用
|
|
187
|
+
const server = crypto.transport.createServer({ maxClients: 10000 })
|
|
188
|
+
if (!server.success)
|
|
189
|
+
throw new Error(server.error.message)
|
|
190
|
+
|
|
191
|
+
// 客户端:通常由 apiClient.init({ transport: { crypto } }) 或 kit.client.create({ transport }) 内部调用
|
|
192
|
+
const client = crypto.transport.createClient({
|
|
193
|
+
keyExchangeUrl: 'https://api.example.com/api/v1/_hai/key-exchange',
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const response = await client.encryptedFetch('https://api.example.com/api/v1/echo', {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
body: JSON.stringify({ hello: 'world' }),
|
|
199
|
+
})
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
import { cache } from '@h-ai/cache'
|
|
204
|
+
import { createRedisTransportKeyStore, crypto } from '@h-ai/crypto'
|
|
205
|
+
|
|
206
|
+
await crypto.init()
|
|
207
|
+
await cache.init({ type: 'redis', host: '127.0.0.1', port: 6379 })
|
|
208
|
+
|
|
209
|
+
const server = crypto.transport.createServer({
|
|
210
|
+
keyStore: createRedisTransportKeyStore({ cache, ttlSeconds: 3600 }),
|
|
211
|
+
})
|
|
212
|
+
if (!server.success)
|
|
213
|
+
throw new Error(server.error.message)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`encryptedFetch()` は標準の `fetch` と同じエラー規則に従います。ネットワーク、鍵交換、リクエスト暗号化、レスポンス復号に失敗すると reject します。呼び出し元でアプリケーションの fetch エラー処理に沿って捕捉してください。
|
|
217
|
+
|
|
218
|
+
プロトコル定数は `crypto.transport.protocol`(または `TRANSPORT_PROTOCOL`)で取得できます:`X-Client-Id`、`X-Encrypted`、既定の `/_hai/key-exchange`。
|
|
219
|
+
|
|
220
|
+
### モジュールの終了
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
// 使用完毕后关闭,释放内部状态
|
|
224
|
+
await crypto.close()
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## エラー処理
|
|
228
|
+
|
|
229
|
+
すべての操作は `HaiResult<T>` を返します:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
const result = crypto.asymmetric.encrypt('data', publicKey)
|
|
233
|
+
if (!result.success) {
|
|
234
|
+
// result.error.code / result.error.message
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## テスト
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
pnpm --filter @h-ai/crypto test
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## License
|
|
245
|
+
|
|
246
|
+
Apache-2.0
|
|
247
|
+
|
|
248
|
+
管理画面のオンライン暗号化デモは Crypto の SM3/SM4 API を直接呼びます。SM4 はランダムな一時鍵と IV を使い、成功表示前に復号を検証します。鍵は保存しません。入力が変わると古い結果を消去し、ランダムなダイジェストや Base64 を暗号処理の結果として表示してはいけません。
|
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# @h-ai/crypto
|
|
2
2
|
|
|
3
|
+
[简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
|
|
4
|
+
|
|
5
|
+
[框架首页](../../README.md) · 相关模块:[@h-ai/cache](../cache/README.md) · [@h-ai/core](../core/README.md) · [@h-ai/reldb](../reldb/README.md)
|
|
6
|
+
|
|
3
7
|
加密模块,提供非对称加密、哈希、对称加密与密码哈希能力。
|
|
4
8
|
|
|
5
9
|
## 支持的能力
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@h-ai/crypto",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.0-alpha.
|
|
4
|
+
"version": "0.1.0-alpha.58",
|
|
5
5
|
"description": "Hai Framework cryptography module (asymmetric, symmetric, and hash).",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"exports": {
|
|
@@ -20,29 +20,29 @@
|
|
|
20
20
|
"access": "public"
|
|
21
21
|
},
|
|
22
22
|
"engines": {
|
|
23
|
-
"node": "
|
|
23
|
+
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"
|
|
27
|
-
"
|
|
26
|
+
"@h-ai/core": "0.1.0-alpha.58",
|
|
27
|
+
"sm-crypto": "^0.5.7"
|
|
28
28
|
},
|
|
29
29
|
"optionalDependencies": {
|
|
30
|
-
"@h-ai/cache": "0.1.0-alpha.
|
|
31
|
-
"@h-ai/reldb": "0.1.0-alpha.
|
|
30
|
+
"@h-ai/cache": "0.1.0-alpha.58",
|
|
31
|
+
"@h-ai/reldb": "0.1.0-alpha.58"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@types/node": "^
|
|
34
|
+
"@types/node": "^26.6.5",
|
|
35
35
|
"rimraf": "^6.1.3",
|
|
36
36
|
"tsup": "^8.5.1",
|
|
37
37
|
"typescript": "^6.0.3",
|
|
38
|
-
"vitest": "^4.1.
|
|
38
|
+
"vitest": "^4.1.11"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"build": "tsup",
|
|
42
42
|
"dev": "tsup --watch",
|
|
43
43
|
"test": "vitest run",
|
|
44
44
|
"test:watch": "vitest",
|
|
45
|
-
"typecheck": "
|
|
45
|
+
"typecheck": "node ../../scripts/typecheck.mjs --noEmit",
|
|
46
46
|
"lint": "eslint .",
|
|
47
47
|
"lint:fix": "eslint . --fix",
|
|
48
48
|
"clean": "rimraf dist .turbo",
|