sm-crypto-wasm 0.2.1 → 0.3.1

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.md CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  [🇨🇳 中文](#zh-阅读下面的中文文档) · [🇺🇸 English](#en-english-documentation) · [🇷🇺 Русский](#ru-документация-на-русском)
4
4
 
5
- > 📖 **完整文档(含每个算法的参数表与可交互 Playground)**:<https://jktantan.github.io/sm-crypto-wasm/>
6
-
7
5
  Chinese cryptography standards (**SM2 / SM3 / SM4 / SM9 / ZUC**) implemented in
8
6
  **AssemblyScript → WebAssembly**. Works on Node 18+ and modern browsers (incl.
9
7
  Web Workers). Algorithms are split into sub-packages and lazy-loaded on demand.
@@ -110,6 +108,32 @@ sm2.decrypt(privateKeyPem, ct, { safeMode: true }); // 失败返回 null 而
110
108
 
111
109
  密钥也可以用裸 hex / `Uint8Array` 传入(32 字节私钥、64/65 字节公钥)。
112
110
 
111
+ ### SM2 —— 密钥封装与混合加密
112
+
113
+ ```ts
114
+ import { sm2 } from 'sm-crypto-wasm';
115
+
116
+ await sm2.ensureLoaded();
117
+
118
+ // KEM:用公钥保护一把随机会话密钥(16 或 32 字节)。
119
+ const { key, encapsulation } = sm2.kemEncrypt(publicKeyPem, 32);
120
+ sm2.kemDecrypt(privateKeyPem, encapsulation, 32); // === key
121
+
122
+ // 混合加密:一次调用完成 KEM + SM4-GCM,适合大消息。
123
+ // 返回版本化 sealed:version || klen || encapLen || encapsulation || iv || tag || ct
124
+ const sealed = sm2.hybridEncrypt(publicKeyPem, '616263');
125
+ sm2.hybridDecrypt(privateKeyPem, sealed); // → '616263'(明文 hex)
126
+
127
+ // sealed 也可转成自描述对象,便于调试或与手工组装互操作。
128
+ const obj = sm2.sealedToObject(sealed);
129
+ sm2.hybridDecrypt(privateKeyPem, sm2.sealedFromObject(obj));
130
+ ```
131
+
132
+ - `hybridEncrypt` 每次调用都新生成 16 字节 DEK 与 12 字节 IV;消息字符串按 **hex** 解释(空消息传 `new Uint8Array(0)`)。
133
+ - hybrid 同时用到 `sm2.wasm` 与 `sm4.wasm`,首次调用前请 `await Promise.all([sm2.ensureLoaded(), sm4.ensureLoaded()])`。
134
+ - `hybridDecrypt` 支持 `{ safeMode: true }`,密文支失败返回 `null`。
135
+ - SM2 没有国标定义的独立 KEM;`kemEncrypt` 是“用 SM2 公钥加密随机密钥”的约定做法。
136
+
113
137
  ### SM2 —— 密钥交换协议(GM/T 0003.3)
114
138
 
115
139
  ```ts
@@ -153,8 +177,14 @@ sm9.decrypt(encKp.masterPrivatePem, 'alice@foo.com', ct); // → '616263'
153
177
  // 注意第一参数是**加密主私钥**(KEM 需要它派生 Ppub-e,与 encrypt 可传主公钥不同)。
154
178
  const { key, encapsulation } = sm9.kemEncrypt(encKp.masterPrivatePem, 'alice@foo.com', 32);
155
179
  sm9.kemDecrypt(encKp.masterPrivatePem, 'alice@foo.com', encapsulation, 32); // === key
180
+
181
+ // 混合加密:主公钥(或主私钥)+ id 封装 16 字节 DEK,再用 SM4-GCM 加密消息。
182
+ const sealed = sm9.hybridEncrypt(encKp.masterPublicPem, 'alice@foo.com', '616263');
183
+ sm9.hybridDecrypt(encKp.masterPrivatePem, 'alice@foo.com', sealed); // → '616263'
156
184
  ```
157
185
 
186
+ > SM9 hybrid 同时用到 `sm9.wasm` 与 `sm4.wasm`:首次调用前请分别 `await sm9.ensureLoaded()` 与 `await sm4.ensureLoaded()`。
187
+
158
188
  ### ZUC —— EEA3 机密性 + EIA3 完整性
159
189
 
160
190
  ```ts
@@ -179,10 +209,10 @@ zuc.zucMac(key, macIv, '616263'); // → 4 字节标签
179
209
 
180
210
  | 模块 | 函数 |
181
211
  |---|---|
182
- | `sm2` | `generateKeyPair`(async) · `getPublicKey` · `sign` · `verify` · `encrypt` · `decrypt` · `createExchangeInitiator`(async) · `createExchangeResponder`(async) |
212
+ | `sm2` | `generateKeyPair`(async) · `getPublicKey` · `sign` · `verify` · `encrypt` · `decrypt` · `kemEncrypt` · `kemDecrypt` · `hybridEncrypt` · `hybridDecrypt` · `sealedToObject` · `sealedFromObject` · `createExchangeInitiator`(async) · `createExchangeResponder`(async) |
183
213
  | `sm3` | `hash` · `hmac` |
184
214
  | `sm4` | `encryptECB`/`decryptECB` · `encryptCBC`/`decryptCBC` · `encryptCTR`/`decryptCTR` · `encryptCFB`/`decryptCFB` · `encryptOFB`/`decryptOFB` · `encryptGCM`/`decryptGCM` |
185
- | `sm9` | `generateKeyPair`(async) · `sign` · `verify` · `encrypt` · `decrypt` · `kemEncrypt` · `kemDecrypt` · `getSignPublicKey` · `getEncPublicKey` · `getUserPublicKey` · `createExchangeInitiator`(async) · `createExchangeResponder`(async) |
215
+ | `sm9` | `generateKeyPair`(async) · `sign` · `verify` · `encrypt` · `decrypt` · `kemEncrypt` · `kemDecrypt` · `hybridEncrypt` · `hybridDecrypt` · `sealedToObject` · `sealedFromObject` · `getSignPublicKey` · `getEncPublicKey` · `getUserPublicKey` · `createExchangeInitiator`(async) · `createExchangeResponder`(async) |
186
216
  | `zuc` | `zucKeystream` · `zucEncrypt` · `zucDecrypt` · `zucMac` · `zucEia3Iv` · `configureReuseGuard` |
187
217
 
188
218
  文档名别名(与主名指向同一实现):`sm4.encryptCTR`/`decryptCTR` 等价 `cryptCTR`、`encryptOFB`/`decryptOFB` 等价 `cryptOFB`;`zuc.encrypt`/`decrypt`/`generateKeystream`/`mac`/`eia3` 等价 `zucEncrypt`/`zucDecrypt`/`zucKeystream`/`zucMac`。
@@ -210,6 +240,8 @@ import { encryptCBC } from 'sm-crypto-wasm/sm4';
210
240
  | 浏览器(经打包器) | `fetch(new URL('../<algo>/<algo>.wasm', import.meta.url))`,由打包器归档并重写 URL |
211
241
  | Web Worker | 加载器不依赖 DOM,Worker 内可用 |
212
242
  | Deno / Bun | 走 Node 同路径 |
243
+ | 微信内置浏览器 | 与浏览器相同(`fetch` + `crypto.getRandomValues`);见[微信与小程序](https://jktantan.github.io/sm-crypto-wasm/guide/wechat) |
244
+ | 微信小程序 | `WXWebAssembly` + `wx.getRandomValues`;见[微信与小程序](https://jktantan.github.io/sm-crypto-wasm/guide/wechat) |
213
245
 
214
246
  ### 打包器支持
215
247
 
@@ -434,6 +466,32 @@ sm2.decrypt(privateKeyPem, ct, { safeMode: true }); // returns null instead
434
466
  Keys may also be passed as raw hex / `Uint8Array` (32-byte private key, 64- or
435
467
  65-byte public key).
436
468
 
469
+ ### SM2 — key encapsulation & hybrid encryption
470
+
471
+ ```ts
472
+ import { sm2 } from 'sm-crypto-wasm';
473
+
474
+ await sm2.ensureLoaded();
475
+
476
+ // KEM: protect a random symmetric key with the public key (16 or 32 bytes).
477
+ const { key, encapsulation } = sm2.kemEncrypt(publicKeyPem, 32);
478
+ sm2.kemDecrypt(privateKeyPem, encapsulation, 32); // === key
479
+
480
+ // Hybrid encryption: one call performs KEM + SM4-GCM; suited to large messages.
481
+ // Returns a versioned sealed value: version || klen || encapLen || encapsulation || iv || tag || ct
482
+ const sealed = sm2.hybridEncrypt(publicKeyPem, '616263');
483
+ sm2.hybridDecrypt(privateKeyPem, sealed); // → '616263' (plaintext hex)
484
+
485
+ // The sealed value can also be inspected as a self-describing object.
486
+ const obj = sm2.sealedToObject(sealed);
487
+ sm2.hybridDecrypt(privateKeyPem, sm2.sealedFromObject(obj));
488
+ ```
489
+
490
+ - Every `hybridEncrypt` call generates a fresh 16-byte DEK and 12-byte IV; message strings are interpreted as **hex** (pass `new Uint8Array(0)` for an empty message).
491
+ - Hybrid uses both `sm2.wasm` and `sm4.wasm`; warm them up with `await Promise.all([sm2.ensureLoaded(), sm4.ensureLoaded()])` before the first call.
492
+ - `hybridDecrypt` supports `{ safeMode: true }`, returning `null` for ciphertext-side failures.
493
+ - SM2 has no standardized KEM construction; `kemEncrypt` is the conventional "encrypt a random key with SM2" idiom.
494
+
437
495
  ### SM2 — key exchange (GM/T 0003.3)
438
496
 
439
497
  ```ts
@@ -479,8 +537,15 @@ sm9.decrypt(encKp.masterPrivatePem, 'alice@foo.com', ct); // → '616263'
479
537
  // derive Ppub-e; encrypt() above can take a master public key instead).
480
538
  const { key, encapsulation } = sm9.kemEncrypt(encKp.masterPrivatePem, 'alice@foo.com', 32);
481
539
  sm9.kemDecrypt(encKp.masterPrivatePem, 'alice@foo.com', encapsulation, 32); // === key
540
+
541
+ // Hybrid encryption: encapsulate a 16-byte DEK with the master public (or private) key + id,
542
+ // then encrypt the message with SM4-GCM.
543
+ const sealed = sm9.hybridEncrypt(encKp.masterPublicPem, 'alice@foo.com', '616263');
544
+ sm9.hybridDecrypt(encKp.masterPrivatePem, 'alice@foo.com', sealed); // → '616263'
482
545
  ```
483
546
 
547
+ > SM9 hybrid uses both `sm9.wasm` and `sm4.wasm`: call `await sm9.ensureLoaded()` and `await sm4.ensureLoaded()` before the first use.
548
+
484
549
  ### ZUC — EEA3 confidentiality + EIA3 integrity
485
550
 
486
551
  ```ts
@@ -505,10 +570,10 @@ zuc.zucMac(key, macIv, '616263'); // → 4-byte tag
505
570
 
506
571
  | Module | Functions |
507
572
  |---|---|
508
- | `sm2` | `generateKeyPair` (async) · `getPublicKey` · `sign` · `verify` · `encrypt` · `decrypt` · `createExchangeInitiator` (async) · `createExchangeResponder` (async) |
573
+ | `sm2` | `generateKeyPair` (async) · `getPublicKey` · `sign` · `verify` · `encrypt` · `decrypt` · `kemEncrypt` · `kemDecrypt` · `hybridEncrypt` · `hybridDecrypt` · `sealedToObject` · `sealedFromObject` · `createExchangeInitiator` (async) · `createExchangeResponder` (async) |
509
574
  | `sm3` | `hash` · `hmac` |
510
575
  | `sm4` | `encryptECB` / `decryptECB` · `encryptCBC` / `decryptCBC` · `encryptCTR` / `decryptCTR` · `encryptCFB` / `decryptCFB` · `encryptOFB` / `decryptOFB` · `encryptGCM` / `decryptGCM` |
511
- | `sm9` | `generateKeyPair` (async) · `sign` · `verify` · `encrypt` · `decrypt` · `kemEncrypt` · `kemDecrypt` · `getSignPublicKey` · `getEncPublicKey` · `getUserPublicKey` · `createExchangeInitiator` (async) · `createExchangeResponder` (async) |
576
+ | `sm9` | `generateKeyPair` (async) · `sign` · `verify` · `encrypt` · `decrypt` · `kemEncrypt` · `kemDecrypt` · `hybridEncrypt` · `hybridDecrypt` · `sealedToObject` · `sealedFromObject` · `getSignPublicKey` · `getEncPublicKey` · `getUserPublicKey` · `createExchangeInitiator` (async) · `createExchangeResponder` (async) |
512
577
  | `zuc` | `zucKeystream` · `zucEncrypt` · `zucDecrypt` · `zucMac` · `zucEia3Iv` · `configureReuseGuard` |
513
578
 
514
579
  Documented aliases (they point to the same implementation):
@@ -541,6 +606,8 @@ Available sub-paths: `sm-crypto-wasm/sm2` · `/sm3` · `/sm4` · `/sm9` · `/zuc
541
606
  | Browser (via a bundler) | `fetch(new URL('../<algo>/<algo>.wasm', import.meta.url))`; the bundler archives and rewrites it |
542
607
  | Web Worker | the loader is DOM-free and works inside a Worker |
543
608
  | Deno / Bun | follows the Node path |
609
+ | WeChat built-in browser | same as browsers (`fetch` + `crypto.getRandomValues`); see [WeChat & Mini Programs](https://jktantan.github.io/sm-crypto-wasm/en/guide/wechat) |
610
+ | WeChat Mini Program | `WXWebAssembly` + `wx.getRandomValues`; see [WeChat & Mini Programs](https://jktantan.github.io/sm-crypto-wasm/en/guide/wechat) |
544
611
 
545
612
  ### Bundler support
546
613
 
@@ -794,6 +861,32 @@ sm2.decrypt(privateKeyPem, ct, { safeMode: true }); // при ошибке
794
861
  Ключи также можно передавать как «сырой» hex или `Uint8Array` (32 байта для
795
862
  закрытого ключа, 64 или 65 байт для открытого).
796
863
 
864
+ ### SM2 — инкапсуляция ключа и гибридное шифрование
865
+
866
+ ```ts
867
+ import { sm2 } from 'sm-crypto-wasm';
868
+
869
+ await sm2.ensureLoaded();
870
+
871
+ // KEM: защитить случайный симметричный ключ открытым ключом (16 или 32 байта).
872
+ const { key, encapsulation } = sm2.kemEncrypt(publicKeyPem, 32);
873
+ sm2.kemDecrypt(privateKeyPem, encapsulation, 32); // === key
874
+
875
+ // Гибридное шифрование: один вызов выполняет KEM + SM4-GCM; подходит для больших сообщений.
876
+ // Возвращает версионированный sealed: version || klen || encapLen || encapsulation || iv || tag || ct
877
+ const sealed = sm2.hybridEncrypt(publicKeyPem, '616263');
878
+ sm2.hybridDecrypt(privateKeyPem, sealed); // → '616263' (открытый текст в hex)
879
+
880
+ // sealed можно представить в виде самоописывающего объекта.
881
+ const obj = sm2.sealedToObject(sealed);
882
+ sm2.hybridDecrypt(privateKeyPem, sm2.sealedFromObject(obj));
883
+ ```
884
+
885
+ - Каждый вызов `hybridEncrypt` создаёт новые 16-байтовый DEK и 12-байтовый IV; строки сообщений интерпретируются как **hex** (для пустого сообщения передайте `new Uint8Array(0)`).
886
+ - Гибридному режиму нужны и `sm2.wasm`, и `sm4.wasm`; перед первым вызовом выполните `await Promise.all([sm2.ensureLoaded(), sm4.ensureLoaded()])`.
887
+ - `hybridDecrypt` поддерживает `{ safeMode: true }` и возвращает `null` при ошибках, связанных с шифртекстом.
888
+ - В SM2 нет стандартизованной конструкции KEM; `kemEncrypt` — общепринятый приём «зашифровать случайный ключ открытым ключом SM2».
889
+
797
890
  ### SM2 — протокол обмена ключами (GM/T 0003.3)
798
891
 
799
892
  ```ts
@@ -839,8 +932,15 @@ sm9.decrypt(encKp.masterPrivatePem, 'alice@foo.com', ct); // → '616263'
839
932
  // (KEM нуждается в нём, чтобы вывести Ppub-e; для encrypt выше допустим мастер-публичный ключ).
840
933
  const { key, encapsulation } = sm9.kemEncrypt(encKp.masterPrivatePem, 'alice@foo.com', 32);
841
934
  sm9.kemDecrypt(encKp.masterPrivatePem, 'alice@foo.com', encapsulation, 32); // === key
935
+
936
+ // Гибридное шифрование: инкапсулировать 16-байтовый DEK мастер-публичным (или приватным)
937
+ // ключом и id, затем зашифровать сообщение через SM4-GCM.
938
+ const sealed = sm9.hybridEncrypt(encKp.masterPublicPem, 'alice@foo.com', '616263');
939
+ sm9.hybridDecrypt(encKp.masterPrivatePem, 'alice@foo.com', sealed); // → '616263'
842
940
  ```
843
941
 
942
+ > Гибридному режиму SM9 нужны и `sm9.wasm`, и `sm4.wasm`: перед первым вызовом выполните `await sm9.ensureLoaded()` и `await sm4.ensureLoaded()`.
943
+
844
944
  ### ZUC — конфиденциальность EEA3 и целостность EIA3
845
945
 
846
946
  ```ts
@@ -865,10 +965,10 @@ zuc.zucMac(key, macIv, '616263'); // → 4-байтовый тег
865
965
 
866
966
  | Модуль | Функции |
867
967
  |---|---|
868
- | `sm2` | `generateKeyPair` (async) · `getPublicKey` · `sign` · `verify` · `encrypt` · `decrypt` · `createExchangeInitiator` (async) · `createExchangeResponder` (async) |
968
+ | `sm2` | `generateKeyPair` (async) · `getPublicKey` · `sign` · `verify` · `encrypt` · `decrypt` · `kemEncrypt` · `kemDecrypt` · `hybridEncrypt` · `hybridDecrypt` · `sealedToObject` · `sealedFromObject` · `createExchangeInitiator` (async) · `createExchangeResponder` (async) |
869
969
  | `sm3` | `hash` · `hmac` |
870
970
  | `sm4` | `encryptECB` / `decryptECB` · `encryptCBC` / `decryptCBC` · `encryptCTR` / `decryptCTR` · `encryptCFB` / `decryptCFB` · `encryptOFB` / `decryptOFB` · `encryptGCM` / `decryptGCM` |
871
- | `sm9` | `generateKeyPair` (async) · `sign` · `verify` · `encrypt` · `decrypt` · `kemEncrypt` · `kemDecrypt` · `getSignPublicKey` · `getEncPublicKey` · `getUserPublicKey` · `createExchangeInitiator` (async) · `createExchangeResponder` (async) |
971
+ | `sm9` | `generateKeyPair` (async) · `sign` · `verify` · `encrypt` · `decrypt` · `kemEncrypt` · `kemDecrypt` · `hybridEncrypt` · `hybridDecrypt` · `sealedToObject` · `sealedFromObject` · `getSignPublicKey` · `getEncPublicKey` · `getUserPublicKey` · `createExchangeInitiator` (async) · `createExchangeResponder` (async) |
872
972
  | `zuc` | `zucKeystream` · `zucEncrypt` · `zucDecrypt` · `zucMac` · `zucEia3Iv` · `configureReuseGuard` |
873
973
 
874
974
  Документированные алиасы (указывают на ту же реализацию):
@@ -901,6 +1001,8 @@ import { encryptCBC } from 'sm-crypto-wasm/sm4';
901
1001
  | Браузер (через сборщик) | `fetch(new URL('../<algo>/<algo>.wasm', import.meta.url))`; сборщик кладёт файл в вывод и переписывает URL |
902
1002
  | Web Worker | загрузчик не зависит от DOM и работает внутри Worker'а |
903
1003
  | Deno / Bun | идут по Node-пути |
1004
+ | Встроенный браузер WeChat | как обычный браузер (`fetch` + `crypto.getRandomValues`); см. [WeChat и мини-программы](https://jktantan.github.io/sm-crypto-wasm/ru/guide/wechat) |
1005
+ | Мини-программа WeChat | `WXWebAssembly` + `wx.getRandomValues`; см. [WeChat и мини-программы](https://jktantan.github.io/sm-crypto-wasm/ru/guide/wechat) |
904
1006
 
905
1007
  ### Поддержка сборщиков
906
1008
 
@@ -0,0 +1,25 @@
1
+ /**
2
+ * 混合加密的共享输入约定。
3
+ *
4
+ * 两个 hybrid 模块(SM2 / SM9)对 message 采用**同一套**字符串语义:
5
+ * - `Uint8Array`:原样使用(拷贝);
6
+ * - `string`:必须是 hex(允许 `0x` 前缀),不再猜 base64 / UTF-8;
7
+ * - 空消息请传 `new Uint8Array(0)`;空字符串按既有 `toBytes` 契约直接拒绝,
8
+ * 避免“空输入静默成功”。
9
+ *
10
+ * IV 同样按 hex / Uint8Array 处理,未提供时由宿主 CSPRNG 生成 12 字节。
11
+ */
12
+ /**
13
+ * 把 hybrid 的 message 归一化为字节。
14
+ *
15
+ * @param input hex 字符串或字节数组。
16
+ * @param label 错误信息里的字段名。
17
+ * @throws TypeError 空字符串、奇数长度、非 hex 字符或类型不对。
18
+ */
19
+ export declare function toMessageBytes(input: string | Uint8Array, label?: string): Uint8Array;
20
+ /**
21
+ * 归一化 hybrid 的 IV:未提供则新生成 12 字节;提供则必须恰好 12 字节。
22
+ *
23
+ * @throws RangeError 长度不是 12。
24
+ */
25
+ export declare function resolveIv(iv: string | Uint8Array | undefined): Uint8Array;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * 混合加密的共享输入约定。
3
+ *
4
+ * 两个 hybrid 模块(SM2 / SM9)对 message 采用**同一套**字符串语义:
5
+ * - `Uint8Array`:原样使用(拷贝);
6
+ * - `string`:必须是 hex(允许 `0x` 前缀),不再猜 base64 / UTF-8;
7
+ * - 空消息请传 `new Uint8Array(0)`;空字符串按既有 `toBytes` 契约直接拒绝,
8
+ * 避免“空输入静默成功”。
9
+ *
10
+ * IV 同样按 hex / Uint8Array 处理,未提供时由宿主 CSPRNG 生成 12 字节。
11
+ */
12
+ import { hexToBytes } from '../runtime/convert.js';
13
+ import { randomBytes } from '../runtime/random.js';
14
+ import { SEALED_IV_LEN } from './sealed.js';
15
+ /**
16
+ * 把 hybrid 的 message 归一化为字节。
17
+ *
18
+ * @param input hex 字符串或字节数组。
19
+ * @param label 错误信息里的字段名。
20
+ * @throws TypeError 空字符串、奇数长度、非 hex 字符或类型不对。
21
+ */
22
+ export function toMessageBytes(input, label = 'message') {
23
+ if (input instanceof Uint8Array)
24
+ return new Uint8Array(input);
25
+ if (typeof input !== 'string') {
26
+ throw new TypeError(`hybrid: ${label} 必须是 hex 字符串或 Uint8Array`);
27
+ }
28
+ if (input.length === 0) {
29
+ throw new TypeError(`hybrid: ${label} 为空;空消息请传 new Uint8Array(0)`);
30
+ }
31
+ return hexToBytes(input, label);
32
+ }
33
+ /**
34
+ * 归一化 hybrid 的 IV:未提供则新生成 12 字节;提供则必须恰好 12 字节。
35
+ *
36
+ * @throws RangeError 长度不是 12。
37
+ */
38
+ export function resolveIv(iv) {
39
+ const out = iv === undefined
40
+ ? randomBytes(SEALED_IV_LEN)
41
+ : iv instanceof Uint8Array
42
+ ? new Uint8Array(iv)
43
+ : hexToBytes(iv, 'iv');
44
+ if (out.length !== SEALED_IV_LEN) {
45
+ throw new RangeError(`hybrid: iv 必须是 ${SEALED_IV_LEN} 字节,实际 ${out.length}`);
46
+ }
47
+ return out;
48
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * 混合加密密文(sealed)的版本化序列化。
3
+ *
4
+ * 布局(全部大端):
5
+ *
6
+ * ```text
7
+ * version(1) || klen(1) || encapLen(2) || encapsulation(encapLen)
8
+ * || iv(12) || tag(16) || ct(rest)
9
+ * ```
10
+ *
11
+ * - `version` 当前为 `1`;未知版本必须拒绝,不能静默按 v1 解析。
12
+ * - `klen` 是 KEM 输出的 DEM 密钥字节数(SM4-128 固定为 16)。
13
+ * - `encapLen` 让解析器能精确切出 KEM 封装段,同时也能识别“长度字段说谎”。
14
+ * - `iv` / `tag` 定长:GCM 推荐 12 字节 IV、16 字节 tag。
15
+ *
16
+ * 对象形态 `SealedObject` 只用于调试 / 自描述场景;线上传输仍建议用单个
17
+ * hex 字符串(`sealedToObject` / `sealedFromObject` 负责互转)。
18
+ *
19
+ * spec/plan 对应:`docs/superpowers/plans/2026-10-16-sm2-sm9-hybrid-encryption.md`
20
+ * Task 2。
21
+ */
22
+ /** 当前 sealed 格式版本。 */
23
+ export declare const SEALED_VERSION = 1;
24
+ /** GCM IV 长度(字节)。 */
25
+ export declare const SEALED_IV_LEN = 12;
26
+ /** GCM tag 长度(字节)。 */
27
+ export declare const SEALED_TAG_LEN = 16;
28
+ /** 头部长度:version + klen + encapLen。 */
29
+ export declare const SEALED_HEADER_LEN = 4;
30
+ /** DEM 密钥长度:SM4-128 固定 16 字节。 */
31
+ export declare const DEM_KEY_LEN = 16;
32
+ /** 解析后的 sealed 各段(字节形态)。 */
33
+ export interface SealedParts {
34
+ version: number;
35
+ klen: number;
36
+ encapsulation: Uint8Array;
37
+ iv: Uint8Array;
38
+ tag: Uint8Array;
39
+ ct: Uint8Array;
40
+ }
41
+ /**
42
+ * sealed 的对象形态(各字段均为 hex)。
43
+ *
44
+ * 适合日志 / 调试 / 与手工 `kemEncrypt + sm4.encryptGCM` 组装互操作。
45
+ */
46
+ export interface SealedObject {
47
+ /** 格式版本,当前必须为 1。 */
48
+ v: number;
49
+ /** KEM 输出的 DEM 密钥字节数。 */
50
+ klen: number;
51
+ /** KEM 封装段。 */
52
+ encapsulation: string;
53
+ /** GCM IV(12 字节)。 */
54
+ iv: string;
55
+ /** GCM tag(16 字节)。 */
56
+ tag: string;
57
+ /** GCM 密文(可为空)。 */
58
+ ct: string;
59
+ }
60
+ /** 把各段组装成 sealed 字节。 */
61
+ export declare function encodeSealed(parts: SealedParts): Uint8Array;
62
+ /**
63
+ * 严格解析 sealed 字节。
64
+ *
65
+ * @throws {InvalidCiphertextError} 版本未知、长度字段与实长不符、任何一段被截断。
66
+ */
67
+ export declare function decodeSealed(sealed: string | Uint8Array): SealedParts;
68
+ /** sealed → 对象形态(各字段 hex)。 */
69
+ export declare function sealedToObject(sealed: string | Uint8Array): SealedObject;
70
+ /**
71
+ * 对象形态 → sealed hex。
72
+ *
73
+ * 严格校验每个字段:版本、klen、encapsulation 非空、iv/tag 定长。
74
+ * 任何不合法都抛 `InvalidCiphertextError`(对象本身描述的是密文)。
75
+ */
76
+ export declare function sealedFromObject(obj: SealedObject): string;
@@ -0,0 +1,160 @@
1
+ /**
2
+ * 混合加密密文(sealed)的版本化序列化。
3
+ *
4
+ * 布局(全部大端):
5
+ *
6
+ * ```text
7
+ * version(1) || klen(1) || encapLen(2) || encapsulation(encapLen)
8
+ * || iv(12) || tag(16) || ct(rest)
9
+ * ```
10
+ *
11
+ * - `version` 当前为 `1`;未知版本必须拒绝,不能静默按 v1 解析。
12
+ * - `klen` 是 KEM 输出的 DEM 密钥字节数(SM4-128 固定为 16)。
13
+ * - `encapLen` 让解析器能精确切出 KEM 封装段,同时也能识别“长度字段说谎”。
14
+ * - `iv` / `tag` 定长:GCM 推荐 12 字节 IV、16 字节 tag。
15
+ *
16
+ * 对象形态 `SealedObject` 只用于调试 / 自描述场景;线上传输仍建议用单个
17
+ * hex 字符串(`sealedToObject` / `sealedFromObject` 负责互转)。
18
+ *
19
+ * spec/plan 对应:`docs/superpowers/plans/2026-10-16-sm2-sm9-hybrid-encryption.md`
20
+ * Task 2。
21
+ */
22
+ import { bytesToHex, hexToBytes, toBytes } from '../runtime/convert.js';
23
+ import { InvalidCiphertextError } from '../runtime/errors.js';
24
+ /** 当前 sealed 格式版本。 */
25
+ export const SEALED_VERSION = 1;
26
+ /** GCM IV 长度(字节)。 */
27
+ export const SEALED_IV_LEN = 12;
28
+ /** GCM tag 长度(字节)。 */
29
+ export const SEALED_TAG_LEN = 16;
30
+ /** 头部长度:version + klen + encapLen。 */
31
+ export const SEALED_HEADER_LEN = 4;
32
+ /** DEM 密钥长度:SM4-128 固定 16 字节。 */
33
+ export const DEM_KEY_LEN = 16;
34
+ /** 把各段组装成 sealed 字节。 */
35
+ export function encodeSealed(parts) {
36
+ const { version, klen, encapsulation, iv, tag, ct } = parts;
37
+ if (!Number.isInteger(version) || version < 0 || version > 0xff) {
38
+ throw new RangeError(`sealed: version 必须是 [0, 255] 内的整数,实际 ${version}`);
39
+ }
40
+ if (!Number.isInteger(klen) || klen <= 0 || klen > 0xff) {
41
+ throw new RangeError(`sealed: klen 必须是 [1, 255] 内的整数,实际 ${klen}`);
42
+ }
43
+ if (encapsulation.length > 0xffff) {
44
+ throw new RangeError(`sealed: encapsulation 不能超过 65535 字节,实际 ${encapsulation.length}`);
45
+ }
46
+ if (iv.length !== SEALED_IV_LEN) {
47
+ throw new RangeError(`sealed: iv 必须是 ${SEALED_IV_LEN} 字节,实际 ${iv.length}`);
48
+ }
49
+ if (tag.length !== SEALED_TAG_LEN) {
50
+ throw new RangeError(`sealed: tag 必须是 ${SEALED_TAG_LEN} 字节,实际 ${tag.length}`);
51
+ }
52
+ const out = new Uint8Array(SEALED_HEADER_LEN + encapsulation.length + SEALED_IV_LEN + SEALED_TAG_LEN + ct.length);
53
+ out[0] = version;
54
+ out[1] = klen;
55
+ out[2] = (encapsulation.length >>> 8) & 0xff;
56
+ out[3] = encapsulation.length & 0xff;
57
+ let off = SEALED_HEADER_LEN;
58
+ out.set(encapsulation, off);
59
+ off += encapsulation.length;
60
+ out.set(iv, off);
61
+ off += SEALED_IV_LEN;
62
+ out.set(tag, off);
63
+ off += SEALED_TAG_LEN;
64
+ out.set(ct, off);
65
+ return out;
66
+ }
67
+ /** 把输入归一化成 sealed 字节。空输入按密文支错误处理。 */
68
+ function sealedBytes(sealed) {
69
+ if (typeof sealed === 'string' && sealed.length === 0) {
70
+ throw new InvalidCiphertextError('sealed: 密文为空');
71
+ }
72
+ if (sealed instanceof Uint8Array && sealed.length === 0) {
73
+ throw new InvalidCiphertextError('sealed: 密文为空');
74
+ }
75
+ return toBytes(sealed, 'sealed');
76
+ }
77
+ /**
78
+ * 严格解析 sealed 字节。
79
+ *
80
+ * @throws {InvalidCiphertextError} 版本未知、长度字段与实长不符、任何一段被截断。
81
+ */
82
+ export function decodeSealed(sealed) {
83
+ const b = sealedBytes(sealed);
84
+ if (b.length < SEALED_HEADER_LEN) {
85
+ throw new InvalidCiphertextError(`sealed: 长度不足(${b.length} 字节,至少需要 4 字节头部)`);
86
+ }
87
+ const version = b[0] ?? 0;
88
+ if (version !== SEALED_VERSION) {
89
+ throw new InvalidCiphertextError(`sealed: 未知版本 ${version}(当前支持 ${SEALED_VERSION})`);
90
+ }
91
+ const klen = b[1] ?? 0;
92
+ if (klen === 0) {
93
+ throw new InvalidCiphertextError('sealed: klen 不能为 0');
94
+ }
95
+ const encapLen = ((b[2] ?? 0) << 8) | (b[3] ?? 0);
96
+ const fixed = SEALED_HEADER_LEN + encapLen + SEALED_IV_LEN + SEALED_TAG_LEN;
97
+ if (b.length < fixed) {
98
+ throw new InvalidCiphertextError(`sealed: 长度字段 encapLen=${encapLen} 与实长 ${b.length} 不符(至少需要 ${fixed} 字节)`);
99
+ }
100
+ let off = SEALED_HEADER_LEN;
101
+ const encapsulation = b.slice(off, off + encapLen);
102
+ off += encapLen;
103
+ const iv = b.slice(off, off + SEALED_IV_LEN);
104
+ off += SEALED_IV_LEN;
105
+ const tag = b.slice(off, off + SEALED_TAG_LEN);
106
+ off += SEALED_TAG_LEN;
107
+ const ct = b.slice(off);
108
+ return { version, klen, encapsulation, iv, tag, ct };
109
+ }
110
+ /** sealed → 对象形态(各字段 hex)。 */
111
+ export function sealedToObject(sealed) {
112
+ const p = decodeSealed(sealed);
113
+ return {
114
+ v: p.version,
115
+ klen: p.klen,
116
+ encapsulation: bytesToHex(p.encapsulation),
117
+ iv: bytesToHex(p.iv),
118
+ tag: bytesToHex(p.tag),
119
+ ct: bytesToHex(p.ct),
120
+ };
121
+ }
122
+ /**
123
+ * 对象形态 → sealed hex。
124
+ *
125
+ * 严格校验每个字段:版本、klen、encapsulation 非空、iv/tag 定长。
126
+ * 任何不合法都抛 `InvalidCiphertextError`(对象本身描述的是密文)。
127
+ */
128
+ export function sealedFromObject(obj) {
129
+ if (obj === null || typeof obj !== 'object') {
130
+ throw new InvalidCiphertextError('sealed: 对象形态必须是 { v, klen, encapsulation, iv, tag, ct }');
131
+ }
132
+ if (typeof obj.v !== 'number' ||
133
+ typeof obj.klen !== 'number' ||
134
+ typeof obj.encapsulation !== 'string' ||
135
+ typeof obj.iv !== 'string' ||
136
+ typeof obj.tag !== 'string' ||
137
+ typeof obj.ct !== 'string') {
138
+ throw new InvalidCiphertextError('sealed: 对象字段类型必须是 { v:number, klen:number, encapsulation/iv/tag/ct:string }');
139
+ }
140
+ if (obj.v !== SEALED_VERSION) {
141
+ throw new InvalidCiphertextError(`sealed: 未知版本 ${String(obj.v)}(当前支持 ${SEALED_VERSION})`);
142
+ }
143
+ if (!Number.isInteger(obj.klen) || obj.klen <= 0 || obj.klen > 0xff) {
144
+ throw new InvalidCiphertextError(`sealed: klen 必须是 [1, 255] 内的整数,实际 ${String(obj.klen)}`);
145
+ }
146
+ const encapsulation = hexToBytes(obj.encapsulation, 'encapsulation');
147
+ const iv = hexToBytes(obj.iv, 'iv');
148
+ const tag = hexToBytes(obj.tag, 'tag');
149
+ const ct = obj.ct.length === 0 ? new Uint8Array(0) : hexToBytes(obj.ct, 'ct');
150
+ if (encapsulation.length === 0) {
151
+ throw new InvalidCiphertextError('sealed: encapsulation 不能为空');
152
+ }
153
+ if (iv.length !== SEALED_IV_LEN) {
154
+ throw new InvalidCiphertextError(`sealed: iv 必须是 ${SEALED_IV_LEN} 字节,实际 ${iv.length}`);
155
+ }
156
+ if (tag.length !== SEALED_TAG_LEN) {
157
+ throw new InvalidCiphertextError(`sealed: tag 必须是 ${SEALED_TAG_LEN} 字节,实际 ${tag.length}`);
158
+ }
159
+ return bytesToHex(encodeSealed({ version: obj.v, klen: obj.klen, encapsulation, iv, tag, ct }));
160
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * base64 纯 JS 实现(宿主既没有 `Buffer`、也没有 `btoa` / `atob` 时的兜底)。
3
+ *
4
+ * 存在理由:微信小程序等运行时没有 `btoa` / `atob`,而 SM2 / SM9 的 PEM 密钥
5
+ * 生成与解析需要 base64。base64 是确定性编码、不属于密码学原语,因此在库内兜底
6
+ * 不需要像 RNG 那样 fail-closed。
7
+ *
8
+ * 只在 `runtime/convert.ts` 的最后一级回退里使用;Node 有 `Buffer`、浏览器有
9
+ * `btoa` / `atob` 时都不会走到这里。零 `node:*` 引用,可安全进入浏览器/小程序产物。
10
+ */
11
+ /** 标准 base64 编码(带 `=` padding)。 */
12
+ export declare function encodeBase64(bytes: Uint8Array): string;
13
+ /**
14
+ * 标准 base64 解码。
15
+ *
16
+ * 与 `atob` 的宽松度对齐:允许空白字符与缺失 padding;非法字符、非法长度抛
17
+ * `TypeError`(`convert.base64ToBytes` 的契约是失败抛 TypeError)。
18
+ *
19
+ * @param b64 base64 字符串。
20
+ * @param label 错误信息里的字段名。
21
+ */
22
+ export declare function decodeBase64(b64: string, label?: string): Uint8Array;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * base64 纯 JS 实现(宿主既没有 `Buffer`、也没有 `btoa` / `atob` 时的兜底)。
3
+ *
4
+ * 存在理由:微信小程序等运行时没有 `btoa` / `atob`,而 SM2 / SM9 的 PEM 密钥
5
+ * 生成与解析需要 base64。base64 是确定性编码、不属于密码学原语,因此在库内兜底
6
+ * 不需要像 RNG 那样 fail-closed。
7
+ *
8
+ * 只在 `runtime/convert.ts` 的最后一级回退里使用;Node 有 `Buffer`、浏览器有
9
+ * `btoa` / `atob` 时都不会走到这里。零 `node:*` 引用,可安全进入浏览器/小程序产物。
10
+ */
11
+ const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
12
+ /** 标准 base64 编码(带 `=` padding)。 */
13
+ export function encodeBase64(bytes) {
14
+ let out = '';
15
+ for (let i = 0; i < bytes.length; i += 3) {
16
+ const a = bytes[i] ?? 0;
17
+ const hasB = i + 1 < bytes.length;
18
+ const hasC = i + 2 < bytes.length;
19
+ const b = hasB ? (bytes[i + 1] ?? 0) : 0;
20
+ const c = hasC ? (bytes[i + 2] ?? 0) : 0;
21
+ const n = (a << 16) | (b << 8) | c;
22
+ out += B64.charAt((n >> 18) & 63);
23
+ out += B64.charAt((n >> 12) & 63);
24
+ out += hasB ? B64.charAt((n >> 6) & 63) : '=';
25
+ out += hasC ? B64.charAt(n & 63) : '=';
26
+ }
27
+ return out;
28
+ }
29
+ /**
30
+ * 标准 base64 解码。
31
+ *
32
+ * 与 `atob` 的宽松度对齐:允许空白字符与缺失 padding;非法字符、非法长度抛
33
+ * `TypeError`(`convert.base64ToBytes` 的契约是失败抛 TypeError)。
34
+ *
35
+ * @param b64 base64 字符串。
36
+ * @param label 错误信息里的字段名。
37
+ */
38
+ export function decodeBase64(b64, label = 'base64') {
39
+ const stripped = b64.replace(/[\t\n\f\r ]/g, '');
40
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(stripped)) {
41
+ throw new TypeError(`${label}: invalid base64`);
42
+ }
43
+ const pad = stripped.endsWith('==') ? 2 : stripped.endsWith('=') ? 1 : 0;
44
+ if (pad > 0 && stripped.length % 4 !== 0) {
45
+ throw new TypeError(`${label}: invalid base64 padding`);
46
+ }
47
+ const body = pad > 0 ? stripped.slice(0, -pad) : stripped;
48
+ if (body.length % 4 === 1) {
49
+ throw new TypeError(`${label}: invalid base64 length`);
50
+ }
51
+ const out = new Uint8Array(Math.floor((body.length * 3) / 4));
52
+ let buffer = 0;
53
+ let bits = 0;
54
+ let pos = 0;
55
+ for (let i = 0; i < body.length; i++) {
56
+ const v = B64.indexOf(body.charAt(i));
57
+ if (v < 0)
58
+ throw new TypeError(`${label}: invalid base64 character at index ${i}`);
59
+ buffer = (buffer << 6) | v;
60
+ bits += 6;
61
+ if (bits >= 8) {
62
+ bits -= 8;
63
+ out[pos++] = (buffer >> bits) & 0xff;
64
+ buffer &= (1 << bits) - 1;
65
+ }
66
+ }
67
+ return out.subarray(0, pos);
68
+ }
@@ -40,10 +40,19 @@ export declare function fromBytes(bytes: Uint8Array, format?: OutputFormat): str
40
40
  export declare function bytesToHex(bytes: Uint8Array): string;
41
41
  /** hex 字符串 → bytes。支持 `0x` 前缀。奇数位长度或非法字符抛错。 */
42
42
  export declare function hexToBytes(hex: string, label?: string): Uint8Array;
43
- /** bytes → base64。Node `Buffer` 优先,浏览器回落到 `btoa`。 */
43
+ /**
44
+ * bytes → base64。优先级:Node `Buffer` → 宿主 `btoa` → 纯 JS 兜底。
45
+ *
46
+ * 微信小程序既没有 `Buffer` 也没有 `btoa`,会走最后一级;不需要宿主再安装
47
+ * base64 polyfill。
48
+ */
44
49
  export declare function bytesToBase64(bytes: Uint8Array): string;
45
- /** base64 字符串 → bytes。失败抛 TypeError。 */
46
- export declare function base64ToBytes(b64: string, _label?: string): Uint8Array;
50
+ /**
51
+ * base64 字符串 → bytes。失败抛 TypeError。
52
+ *
53
+ * 优先级:Node `Buffer` → 宿主 `atob` → 纯 JS 兜底(微信小程序等)。
54
+ */
55
+ export declare function base64ToBytes(b64: string, label?: string): Uint8Array;
47
56
  /** PEM → bytes。剥去 BEGIN/END armor,base64 解码。 */
48
57
  export declare function pemToBytes(pem: string, label?: string): Uint8Array;
49
58
  /** bytes → PEM(带 BEGIN/END 头)。`kind` 例:`SM2 PRIVATE KEY`。 */