@h-ai/crypto 1.0.0-beta.1 → 1.0.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -156,6 +156,8 @@ if (hashed.success) {
156
156
 
157
157
  #### Transport encryption (crypto.transport)
158
158
 
159
+ 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.
160
+
159
161
  See the [Transport v2 wire protocol](TRANSPORT-PROTOCOL.md) for canonical encoding, verification order and replay persistence.
160
162
 
161
163
  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.
@@ -246,7 +248,7 @@ const serverKeyRing = ring.data
246
248
  const server = crypto.transport.createServer({ serverKeyRing, keyStore })
247
249
  ```
248
250
 
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 tracked `.env.dev/.env.test/.env.prod`. Clones share each environment’s ring; private environment variables or a Secret Manager can override it. Runtime does not generate keys.
251
+ 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
252
 
251
253
  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
254
 
package/README.ja.md CHANGED
@@ -21,6 +21,8 @@
21
21
 
22
22
  ### セキュリティに関する注意
23
23
 
24
+ 伝送クライアントの `destroy()` は進行中のハンドシェイクを無効化して中断し、遅延結果による鍵の復活や次回ハンドシェイクの上書きを防ぎます。サーバー registry は自身の期限で回収します。
25
+
24
26
  - **SM4 の既定の暗号化結果は構造化された暗号文**:`crypto.symmetric.encrypt(data, key)` は既定で CBC + ランダム IV を使用し、`{ mode, ciphertext, iv, encoding }` を返します。復号時はこのオブジェクトを `crypto.symmetric.decrypt(payload, key)` に渡します。
25
27
  - **SM4 にはランダムな鍵を使用してください**:`crypto.symmetric.generateKey()` で鍵を生成します。このモジュールはパスワードから暗号鍵を導出する KDF を提供しません。パスワードの保存にはサーバー専用の `crypto.password.hash(password)` を使用し、パスワードやそのハッシュを直接 SM4 の鍵にしないでください。
26
28
  - **IV は毎回一意にしてください**:同じ鍵で IV を再利用してはいけません。既定の CBC または `encryptWithIV()` による毎回の自動生成を推奨します。IV は暗号文と一緒に送信・保存します(公開可能で、秘密にする必要はありません)。
@@ -246,7 +248,7 @@ const serverKeyRing = ring.data
246
248
  const server = crypto.transport.createServer({ serverKeyRing, keyStore })
247
249
  ```
248
250
 
249
- 鍵交換は `{ serverKeyId, serverPublicKey, clientId }` を返し、暗号化リクエストは `serverKeyId` を送ります。サーバーは指定した秘密鍵だけを選び、不明・失効 ID は他の鍵を試さず拒否します。応答はクライアント公開鍵で暗号化します。更新時は全ノードに新旧の鍵を先に配布し、続いて `activeKeyId` を切り替えて manager を順次再構築します。移行期間後に古い ID を全ノードから削除します。旧クライアントは `destroy()` 後に再交渉し、非冪等処理を自動再送しません。同じ鍵環を Secret Manager またはサーバー専用の非公開環境から配布し、通常の YAML、PUBLIC_/VITE_、ログ、公開応答には含めないでください。CLI は `.env.dev/.env.test/.env.prod` ごとに独立した鍵環を作成し、Git に保存します。クローン後も環境ごとに共有でき、非公開環境や Secret Manager で上書きできます。実行時には生成しません。
251
+ 鍵交換は `{ 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
252
 
251
253
  認証済み 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
254
 
package/README.md CHANGED
@@ -156,6 +156,8 @@ if (hashed.success) {
156
156
 
157
157
  #### 传输加密(crypto.transport)
158
158
 
159
+ 传输客户端 `destroy()` 使在途握手失效并取消网络等待;迟到结果不能恢复密钥或覆盖下一次握手。它只清理本地会话,服务端 registry 仍按自己的租约回收。
160
+
159
161
  规范编码、验证顺序与 nonce 持久化要求见 [Transport v2 协议](TRANSPORT-PROTOCOL.md)。
160
162
 
161
163
  传输加密统一通过 `crypto.transport` 命名空间提供,供 `@h-ai/serv`、`@h-ai/kit` 与 `@h-ai/api-client` 复用同一套协议常量和载荷格式。
@@ -246,7 +248,7 @@ const serverKeyRing = ring.data
246
248
  const server = crypto.transport.createServer({ serverKeyRing, keyStore })
247
249
  ```
248
250
 
249
- 握手返回 `{ serverKeyId, serverPublicKey, clientId }`;请求密文的 `context` 携带 `serverKeyId`,服务端按 ID 选择私钥,未知或撤销 ID 直接拒绝,不尝试其它密钥。响应仍用客户端公钥加密。轮换先向所有节点分发包含新旧版本的 ring,再切换 `activeKeyId` 并滚动重建 manager;过渡期保留旧版本,结束后删除旧 ID 并更新所有节点。旧客户端此后须 `destroy()` 后重新协商;不要自动重试非幂等业务。部署通过 Secret Manager 或服务端私有环境分发同一 ring,禁止写入普通 YAML、PUBLIC_/VITE_、日志或公开响应。CLI 创建服务端项目时为 `.env.dev/.env.test/.env.prod` 分别生成独立密钥环并随项目提交;克隆后同一环境共享,也可通过私有环境或 Secret Manager 覆盖。运行时不生成密钥。
251
+ 握手返回 `{ serverKeyId, serverPublicKey, clientId }`;请求密文的 `context` 携带 `serverKeyId`,服务端按 ID 选择私钥,未知或撤销 ID 直接拒绝,不尝试其它密钥。响应仍用客户端公钥加密。轮换先向所有节点分发包含新旧版本的 ring,再切换 `activeKeyId` 并滚动重建 manager;过渡期保留旧版本,结束后删除旧 ID 并更新所有节点。旧客户端此后须 `destroy()` 后重新协商;不要自动重试非幂等业务。部署通过 Secret Manager 或服务端私有环境分发同一 ring,禁止写入普通 YAML、PUBLIC_/VITE_、日志或公开响应。CLI 创建服务端项目时为 `.env.dev.local/.env.test.local/.env.prod.local` 分别生成独立密钥环,文件不提交 Git;克隆后由受保护配置或 Secret Manager 分发对应环境秘密,也可通过私有环境或 Secret Manager 覆盖。运行时不生成密钥。
250
252
 
251
253
  认证协议仅支持 v2:独立随机 SM4-CBC 密钥和 HMAC-SM3 密钥由 SM2 封装,先验签、再验证 MAC,最后解密 CBC。请求由客户端私钥签名,响应由所选服务端版本签名。认证字段包括 version、direction、serverKeyId、clientId、128 位随机 requestId(兼 nonce)、issuedAt、HTTP method、pathname + 原始 search 的 SM3 pathHash,以及响应 status。请求有效期 5 分钟,未来时钟偏差最多 30 秒;共享 registry 原子消费 (clientId, requestId),nonce 至少保留到 issuedAt + 6 分钟以覆盖节点偏差,存储故障拒绝请求。Redis 必须保留有效 nonce,使用 noeviction/可靠持久化策略;各节点时钟须同步。此能力防止同一传输消息重放,业务重试仍需自己的幂等键。
252
254
 
package/dist/index.js CHANGED
@@ -1125,7 +1125,9 @@ function createTransportClient(options) {
1125
1125
  let serverKeyId = null;
1126
1126
  let clientId = null;
1127
1127
  let initPromise = null;
1128
- async function doInit() {
1128
+ let generation = 0;
1129
+ let handshakeController;
1130
+ async function doInit(attemptGeneration) {
1129
1131
  const kpResult = options.crypto.asymmetric.generateKeyPair();
1130
1132
  if (!kpResult.success)
1131
1133
  return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportClientKeyGenerateFailed"), kpResult.error);
@@ -1133,6 +1135,7 @@ function createTransportClient(options) {
1133
1135
  if (new TextEncoder().encode(localKeyPair.publicKey).byteLength > TRANSPORT_BODY_LIMITS.publicKeyBytes)
1134
1136
  return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportClientKeyGenerateFailed"));
1135
1137
  const controller = new AbortController();
1138
+ handshakeController = controller;
1136
1139
  const timeout = setTimeout(() => controller.abort(new DOMException("Key exchange timed out", "TimeoutError")), 3e4);
1137
1140
  try {
1138
1141
  let response;
@@ -1169,6 +1172,8 @@ function createTransportClient(options) {
1169
1172
  } catch {
1170
1173
  return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportBodyLimitInvalid"));
1171
1174
  }
1175
+ if (attemptGeneration !== generation || controller.signal.aborted)
1176
+ return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportClientNotInitialized"));
1172
1177
  keyPair = localKeyPair;
1173
1178
  serverPublicKey = body.serverPublicKey;
1174
1179
  serverKeyId = body.serverKeyId;
@@ -1176,17 +1181,20 @@ function createTransportClient(options) {
1176
1181
  return ok(void 0);
1177
1182
  } finally {
1178
1183
  clearTimeout(timeout);
1184
+ if (handshakeController === controller)
1185
+ handshakeController = void 0;
1179
1186
  }
1180
1187
  }
1181
1188
  async function ensureReady() {
1182
1189
  if (clientId && serverPublicKey)
1183
1190
  return ok(void 0);
1184
1191
  if (!initPromise) {
1185
- initPromise = doInit().then((result) => {
1186
- if (!result.success)
1192
+ const pending = doInit(generation).then((result) => {
1193
+ if (!result.success && initPromise === pending)
1187
1194
  initPromise = null;
1188
1195
  return result;
1189
1196
  });
1197
+ initPromise = pending;
1190
1198
  }
1191
1199
  return initPromise;
1192
1200
  }
@@ -1360,6 +1368,9 @@ function createTransportClient(options) {
1360
1368
  encryptedFetch,
1361
1369
  ready: () => clientId !== null && serverPublicKey !== null,
1362
1370
  destroy() {
1371
+ generation++;
1372
+ handshakeController?.abort();
1373
+ handshakeController = void 0;
1363
1374
  keyPair = null;
1364
1375
  serverPublicKey = null;
1365
1376
  serverKeyId = null;