sm-crypto-wasm 0.1.2 → 0.3.0

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.
@@ -7,6 +7,8 @@
7
7
  * - `verify(publicKey, message, signature)`:验签
8
8
  * - `encrypt(publicKey, message, options?)`:加密(默认 C1C3C2)
9
9
  * - `decrypt(privateKey, ciphertext)`:解密
10
+ * - `kemEncrypt` / `kemDecrypt`:KEM(用公钥保护随机对称密钥,见 `./kem.ts`)
11
+ * - `hybridEncrypt` / `hybridDecrypt`:KEM + SM4-GCM 混合加密(见 `./hybrid.ts`)
10
12
  *
11
13
  * 说明:
12
14
  * - 密钥输入同时接受裸字节(hex)与 PEM(PKCS#8/SEC1 私钥、SPKI 公钥)。
@@ -70,14 +72,54 @@ export interface Sm2DecryptOptions {
70
72
  * @returns PKCS#8 私钥 PEM 与 SPKI 公钥 PEM。
71
73
  */
72
74
  export declare function generateKeyPair(): Promise<Sm2KeyPair>;
75
+ /**
76
+ * 池中的一个预计算签名点。
77
+ *
78
+ * `point`(`k·G` 的坐标)是公开信息;**`k` 被封在模块内部**,外部读不到,被
79
+ * `sign()` 取走后立刻从索引中摘除并抹掉字节 —— 所以"复用同一个 `k`"在 API 层面
80
+ * 就不可能发生,调用方不需要(也无法)自己去保证这一点。
81
+ */
82
+ export declare class SignaturePoint {
83
+ /** `k·G` 的仿射坐标 `x ‖ y`(64 字节 hex)。公开信息,可用于审计。 */
84
+ readonly point: string;
85
+ /** 只允许本模块的 `createSignaturePointPool` 构造。 */
86
+ private constructor();
87
+ /** @internal 仅供 `createSignaturePointPool` 使用。 */
88
+ static _create(nonce: Uint8Array, point: string): SignaturePoint;
89
+ }
90
+ /** `sign` 的选项。 */
91
+ export interface SignOptions {
92
+ /**
93
+ * 预计算的签名点池(见 `createSignaturePointPool`)。
94
+ *
95
+ * 命中时省掉本次签名的固定基点标量乘(实测占单次签名的 56%);池被取空后
96
+ * 自动回退到现算。**该数组会被原地消费**(每签一次 `pop()` 一个点),
97
+ * 调用方无需自己维护游标。
98
+ */
99
+ pointPool?: SignaturePoint[];
100
+ }
101
+ /**
102
+ * 预计算 `n` 个签名点,供 `sign()` 的 `pointPool` 选项消费。
103
+ *
104
+ * 每个点 = 一次 `k·G`(本机约 130 µs)。批量签名时把这些固定基点标量乘提前
105
+ * 摊销,签名的关键路径只剩 `e = SM3(Z ‖ M)` 与两条模运算 —— 实测快 5.9×。
106
+ *
107
+ * `k` 取自宿主 CSPRNG,并由本模块独占保管:调用方拿不到它,也就无从误用成复用。
108
+ * 唯一的约定是"同一个点只喂一次",而真喂两次会直接抛错。
109
+ *
110
+ * @param n 生成个数。
111
+ * @returns `n` 个签名点。
112
+ */
113
+ export declare function createSignaturePointPool(n: number): Promise<SignaturePoint[]>;
73
114
  /**
74
115
  * SM2 签名(默认 SM3withSM2、默认用户标识)。
75
116
  *
76
117
  * @param privateKey 32 字节私钥。
77
118
  * @param message 待签名消息。
119
+ * @param opts `pointPool`:预计算的签名点池(见 `createSignaturePointPool`)。
78
120
  * @returns 64 字节签名(r || s)的 hex。
79
121
  */
80
- export declare function sign(privateKey: string | Uint8Array, message: string | Uint8Array): string;
122
+ export declare function sign(privateKey: string | Uint8Array, message: string | Uint8Array, opts?: SignOptions): string;
81
123
  /**
82
124
  * SM2 验签。
83
125
  *
@@ -109,3 +151,5 @@ export declare function decrypt(privateKey: string | Uint8Array, ciphertext: str
109
151
  }): string | null;
110
152
  export declare function decrypt(privateKey: string | Uint8Array, ciphertext: string | Uint8Array, opts?: Sm2DecryptOptions): string;
111
153
  export * from './exchange.js';
154
+ export * from './hybrid.js';
155
+ export * from './kem.js';
package/dist/sm2/index.js CHANGED
@@ -7,6 +7,8 @@
7
7
  * - `verify(publicKey, message, signature)`:验签
8
8
  * - `encrypt(publicKey, message, options?)`:加密(默认 C1C3C2)
9
9
  * - `decrypt(privateKey, ciphertext)`:解密
10
+ * - `kemEncrypt` / `kemDecrypt`:KEM(用公钥保护随机对称密钥,见 `./kem.ts`)
11
+ * - `hybridEncrypt` / `hybridDecrypt`:KEM + SM4-GCM 混合加密(见 `./hybrid.ts`)
10
12
  *
11
13
  * 说明:
12
14
  * - 密钥输入同时接受裸字节(hex)与 PEM(PKCS#8/SEC1 私钥、SPKI 公钥)。
@@ -215,34 +217,122 @@ function randomK() {
215
217
  }
216
218
  return out;
217
219
  }
220
+ /**
221
+ * 池中的一个预计算签名点。
222
+ *
223
+ * `point`(`k·G` 的坐标)是公开信息;**`k` 被封在模块内部**,外部读不到,被
224
+ * `sign()` 取走后立刻从索引中摘除并抹掉字节 —— 所以"复用同一个 `k`"在 API 层面
225
+ * 就不可能发生,调用方不需要(也无法)自己去保证这一点。
226
+ */
227
+ export class SignaturePoint {
228
+ /** `k·G` 的仿射坐标 `x ‖ y`(64 字节 hex)。公开信息,可用于审计。 */
229
+ point;
230
+ /** 只允许本模块的 `createSignaturePointPool` 构造。 */
231
+ constructor(point) {
232
+ this.point = point;
233
+ }
234
+ /** @internal 仅供 `createSignaturePointPool` 使用。 */
235
+ static _create(nonce, point) {
236
+ const p = new SignaturePoint(point);
237
+ POINT_NONCE.set(p, nonce);
238
+ return p;
239
+ }
240
+ }
241
+ /**
242
+ * 点位 → 一次性 `k`。取走即 `delete`:同一个点位对象无论被谁持有引用,都只能
243
+ * 取到一次 `k`。用 `WeakMap` 而非对象属性,是为了连"枚举键名"都做不到。
244
+ */
245
+ const POINT_NONCE = new WeakMap();
246
+ /**
247
+ * 取走签名点的一次性 `k`。
248
+ *
249
+ * 索引里没有(已被取过,或不是本库产出的伪造对象)就抛错 —— 于是"同一个点签两次"
250
+ * 会当场失败,而不是悄悄复用 `k` 把私钥送出去。
251
+ */
252
+ function takeNonce(p) {
253
+ const nonce = POINT_NONCE.get(p);
254
+ if (nonce === undefined) {
255
+ throw new Error('sm2: 该签名点不可用(已被使用过,或不是 createSignaturePointPool 产出的);签名点是一次性的。');
256
+ }
257
+ POINT_NONCE.delete(p);
258
+ return nonce;
259
+ }
260
+ /**
261
+ * 预计算 `n` 个签名点,供 `sign()` 的 `pointPool` 选项消费。
262
+ *
263
+ * 每个点 = 一次 `k·G`(本机约 130 µs)。批量签名时把这些固定基点标量乘提前
264
+ * 摊销,签名的关键路径只剩 `e = SM3(Z ‖ M)` 与两条模运算 —— 实测快 5.9×。
265
+ *
266
+ * `k` 取自宿主 CSPRNG,并由本模块独占保管:调用方拿不到它,也就无从误用成复用。
267
+ * 唯一的约定是"同一个点只喂一次",而真喂两次会直接抛错。
268
+ *
269
+ * @param n 生成个数。
270
+ * @returns `n` 个签名点。
271
+ */
272
+ export async function createSignaturePointPool(n) {
273
+ if (!Number.isInteger(n) || n < 0) {
274
+ throw new RangeError(`sm2: 签名点池大小必须是非负整数,收到 ${String(n)}`);
275
+ }
276
+ await ensureLoaded();
277
+ const { exports, memory } = getModule();
278
+ const pool = [];
279
+ for (let i = 0; i < n; i++) {
280
+ const nonce = randomK();
281
+ const kPtr = exports.alloc(32);
282
+ const ptPtr = exports.alloc(64);
283
+ try {
284
+ new Uint8Array(memory.buffer, kPtr, 32).set(nonce);
285
+ exports.scalarMulGOut(kPtr, ptPtr);
286
+ pool.push(SignaturePoint._create(nonce, fromBytes(new Uint8Array(memory.buffer, ptPtr, 64).slice(), 'hex')));
287
+ }
288
+ finally {
289
+ exports.freeAll();
290
+ }
291
+ }
292
+ return pool;
293
+ }
218
294
  /**
219
295
  * SM2 签名(默认 SM3withSM2、默认用户标识)。
220
296
  *
221
297
  * @param privateKey 32 字节私钥。
222
298
  * @param message 待签名消息。
299
+ * @param opts `pointPool`:预计算的签名点池(见 `createSignaturePointPool`)。
223
300
  * @returns 64 字节签名(r || s)的 hex。
224
301
  */
225
- export function sign(privateKey, message) {
302
+ export function sign(privateKey, message, opts = {}) {
226
303
  const d = toPrivateKeyBytes(privateKey);
227
304
  const m = toBytes(message, 'message');
228
305
  const pub = derivePublicKeyBytes(d);
229
- const { exports } = getModule();
306
+ const { exports, memory } = getModule();
230
307
  const dp = exports.alloc(32);
231
308
  const pxp = exports.alloc(32);
232
309
  const pyp = exports.alloc(32);
233
310
  const mp = exports.alloc(m.length);
234
311
  const kp = exports.alloc(32);
312
+ const xp = exports.alloc(32);
235
313
  const op = exports.alloc(64);
314
+ const pool = opts.pointPool;
236
315
  try {
237
316
  putBytes(d, dp);
238
317
  putBytes(pub.subarray(0, 32), pxp);
239
318
  putBytes(pub.subarray(32, 64), pyp);
240
319
  putBytes(m, mp);
241
- // 最多重试几次 k(极小概率 r/s 为 0 或 r+k==n)。
320
+ // 最多重试几次 k(极小概率 r/s 为 0 或 r+k==n)。池命中时每轮换一个点。
242
321
  for (let attempt = 0; attempt < 16; attempt++) {
243
- const k = randomK();
244
- putBytes(k, kp);
245
- const rc = exports.signOut(dp, pxp, pyp, mp, m.length, kp, op);
322
+ const pooled = pool?.pop();
323
+ let rc;
324
+ if (pooled !== undefined) {
325
+ // 一次性取走 k;此处之后该点不可再用(再喂会抛错)。
326
+ const nonce = takeNonce(pooled);
327
+ putBytes(nonce, kp);
328
+ nonce.fill(0); // JS 侧那份随即抹掉
329
+ new Uint8Array(memory.buffer, xp, 32).set(toBytes(pooled.point, 'point').subarray(0, 32));
330
+ rc = exports.signWithPointOut(dp, pxp, pyp, mp, m.length, kp, xp, op);
331
+ }
332
+ else {
333
+ putBytes(randomK(), kp);
334
+ rc = exports.signOut(dp, pxp, pyp, mp, m.length, kp, op);
335
+ }
246
336
  if (rc === 0) {
247
337
  return fromBytes(readBytes(op, 64), 'hex');
248
338
  }
@@ -250,6 +340,8 @@ export function sign(privateKey, message) {
250
340
  throw new Error('sm2.sign: failed to produce a valid signature after retries');
251
341
  }
252
342
  finally {
343
+ // 抹掉 WASM 内存里那份 k(缓冲区在 freeAll 前仍可写)
344
+ new Uint8Array(memory.buffer, kp, 32).fill(0);
253
345
  exports.freeAll();
254
346
  }
255
347
  }
@@ -388,3 +480,6 @@ function decryptRaw(d, ct) {
388
480
  // SM2 密钥交换协议(GM/T 0003.3-2014):createExchangeInitiator /
389
481
  // createExchangeResponder 由 `./exchange` 提供,在此并入 sm2 命名空间。
390
482
  export * from './exchange.js';
483
+ // SM2 KEM(用公钥保护随机对称密钥)与混合加密(KEM + SM4-GCM)。
484
+ export * from './hybrid.js';
485
+ export * from './kem.js';
@@ -0,0 +1,58 @@
1
+ /**
2
+ * SM2 密钥封装(KEM)—— 用 SM2 公钥保护一把随机对称密钥。
3
+ *
4
+ * **注意:这不是国标定义的“SM2-KEM”。** GM/T 0003 只定义了 SM2 公钥加密,
5
+ * 没有定义独立的 KEM 构造;本模块是业界约定俗成的做法:
6
+ *
7
+ * key = 宿主 CSPRNG 生成的 `klen` 字节对称密钥
8
+ * encapsulation = SM2 加密(key)(完整 C1C3C2 密文,自带 C3 完整性校验)
9
+ *
10
+ * 接收方用 `kemDecrypt` 解出同一把 key。它的安全性完全继承自 SM2 公钥加密:
11
+ * - 每次封装都取新的随机数 `k`(由 `sm2.encrypt` 内部完成);
12
+ * - encapsulation 被篡改时 C3 校验失败,抛 `DecryptionError`;
13
+ * - 错误私钥同样过不了 C3 校验,不会返回垃圾密钥。
14
+ *
15
+ * 若需要“公钥只保护短密钥、数据交给对称算法”的完整用法,直接用
16
+ * `hybridEncrypt` / `hybridDecrypt`(本模块的 KEM + SM4-GCM 组装)。
17
+ *
18
+ * spec/plan 对应:`docs/superpowers/plans/2026-10-16-sm2-sm9-hybrid-encryption.md`
19
+ * Task 1。
20
+ */
21
+ /** KEM 封装结果。 */
22
+ export interface Sm2KemResult {
23
+ /** 会话密钥(hex,`2*klen` 个字符)。 */
24
+ key: string;
25
+ /** SM2 密文(C1C3C2 hex,长度 `97 + klen` 字节)。 */
26
+ encapsulation: string;
27
+ }
28
+ /** `kemDecrypt` 选项。 */
29
+ export interface Sm2KemDecryptOptions {
30
+ /**
31
+ * 解封装失败时返回 `null` 而不抛错。
32
+ *
33
+ * 只覆盖密文支错误(`CiphertextError`:密文结构错 / C3 校验失败);
34
+ * 参数类型错误(`TypeError` / `RangeError`)仍会抛出。
35
+ */
36
+ safeMode?: boolean;
37
+ }
38
+ /**
39
+ * SM2 密钥封装:生成一把随机会话密钥,并用接收方公钥封装。
40
+ *
41
+ * @param publicKey 64 或 65 字节公钥(hex / Uint8Array / PEM)。
42
+ * @param klen 会话密钥字节长度,默认 32。
43
+ * @returns `{ key, encapsulation }`,均为 hex。
44
+ */
45
+ export declare function kemEncrypt(publicKey: string | Uint8Array, klen?: number): Sm2KemResult;
46
+ /**
47
+ * SM2 密钥解封装:从 encapsulation 恢复会话密钥。
48
+ *
49
+ * @param privateKey 32 字节私钥(hex / Uint8Array / PEM)。
50
+ * @param encapsulation `kemEncrypt` 产出的 SM2 密文。
51
+ * @param klen 会话密钥字节长度,默认 32;必须与封装时一致。
52
+ * @returns 会话密钥 hex。
53
+ * @throws {DecryptionError} C3 校验失败、私钥错误,或解出的长度与 `klen` 不符。
54
+ */
55
+ export declare function kemDecrypt(privateKey: string | Uint8Array, encapsulation: string | Uint8Array, klen: number, opts: Sm2KemDecryptOptions & {
56
+ safeMode: true;
57
+ }): string | null;
58
+ export declare function kemDecrypt(privateKey: string | Uint8Array, encapsulation: string | Uint8Array, klen?: number, opts?: Sm2KemDecryptOptions): string;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * SM2 密钥封装(KEM)—— 用 SM2 公钥保护一把随机对称密钥。
3
+ *
4
+ * **注意:这不是国标定义的“SM2-KEM”。** GM/T 0003 只定义了 SM2 公钥加密,
5
+ * 没有定义独立的 KEM 构造;本模块是业界约定俗成的做法:
6
+ *
7
+ * key = 宿主 CSPRNG 生成的 `klen` 字节对称密钥
8
+ * encapsulation = SM2 加密(key)(完整 C1C3C2 密文,自带 C3 完整性校验)
9
+ *
10
+ * 接收方用 `kemDecrypt` 解出同一把 key。它的安全性完全继承自 SM2 公钥加密:
11
+ * - 每次封装都取新的随机数 `k`(由 `sm2.encrypt` 内部完成);
12
+ * - encapsulation 被篡改时 C3 校验失败,抛 `DecryptionError`;
13
+ * - 错误私钥同样过不了 C3 校验,不会返回垃圾密钥。
14
+ *
15
+ * 若需要“公钥只保护短密钥、数据交给对称算法”的完整用法,直接用
16
+ * `hybridEncrypt` / `hybridDecrypt`(本模块的 KEM + SM4-GCM 组装)。
17
+ *
18
+ * spec/plan 对应:`docs/superpowers/plans/2026-10-16-sm2-sm9-hybrid-encryption.md`
19
+ * Task 1。
20
+ */
21
+ import { bytesToHex } from '../runtime/convert.js';
22
+ import { CiphertextError, DecryptionError } from '../runtime/errors.js';
23
+ import { randomBytes } from '../runtime/random.js';
24
+ import { decrypt, encrypt } from './index.js';
25
+ /** `klen` 必须是正整数,且不超过宿主 CSPRNG 单次取数上限(65536)。 */
26
+ function requireKlen(klen) {
27
+ if (!Number.isInteger(klen) || klen <= 0 || klen > 65_536) {
28
+ throw new RangeError(`sm2.kemEncrypt: klen 必须是 [1, 65536] 内的整数,实际 ${klen}`);
29
+ }
30
+ }
31
+ /**
32
+ * SM2 密钥封装:生成一把随机会话密钥,并用接收方公钥封装。
33
+ *
34
+ * @param publicKey 64 或 65 字节公钥(hex / Uint8Array / PEM)。
35
+ * @param klen 会话密钥字节长度,默认 32。
36
+ * @returns `{ key, encapsulation }`,均为 hex。
37
+ */
38
+ export function kemEncrypt(publicKey, klen = 32) {
39
+ requireKlen(klen);
40
+ const key = randomBytes(klen);
41
+ const encapsulation = encrypt(publicKey, key);
42
+ return { key: bytesToHex(key), encapsulation };
43
+ }
44
+ export function kemDecrypt(privateKey, encapsulation, klen = 32, opts = {}) {
45
+ requireKlen(klen);
46
+ try {
47
+ const key = decrypt(privateKey, encapsulation);
48
+ if (key.length !== klen * 2) {
49
+ // C3 已通过,说明私钥正确;长度不符通常是调用方把 klen 传错了。
50
+ throw new DecryptionError(`sm2.kemDecrypt: 解出的密钥长度为 ${key.length / 2} 字节,与 klen=${klen} 不符`);
51
+ }
52
+ return key;
53
+ }
54
+ catch (err) {
55
+ if (opts.safeMode && err instanceof CiphertextError)
56
+ return null;
57
+ throw err;
58
+ }
59
+ }
package/dist/sm2/sm2.wasm CHANGED
Binary file
@@ -7,6 +7,9 @@
7
7
  * 3. 拷入输入、调 `instance.exports.sm3(inputPtr, inputLen, outPtr)`。
8
8
  * 4. 从 outPtr 拷出 32 字节。
9
9
  *
10
+ * HMAC 的 ipad/opad 拼接与两次摘要调用都在 WASM 内完成(`exports.sm3Hmac`),
11
+ * 这一层只做参数归一化与缓冲区搬运。
12
+ *
10
13
  * spec/plan 对应:plan Task 7;spec §6 API、§9 安全姿态。
11
14
  */
12
15
  /**
@@ -22,7 +25,7 @@ export declare function ensureLoaded(): Promise<void>;
22
25
  */
23
26
  export declare function hash(input: string | Uint8Array): string;
24
27
  /**
25
- * HMAC-SM3(同步)。
28
+ * HMAC-SM3(同步,RFC 2104)。
26
29
  *
27
30
  * @param key 任意长度密钥。
28
31
  * @param msg 任意长度消息。
package/dist/sm3/index.js CHANGED
@@ -7,6 +7,9 @@
7
7
  * 3. 拷入输入、调 `instance.exports.sm3(inputPtr, inputLen, outPtr)`。
8
8
  * 4. 从 outPtr 拷出 32 字节。
9
9
  *
10
+ * HMAC 的 ipad/opad 拼接与两次摘要调用都在 WASM 内完成(`exports.sm3Hmac`),
11
+ * 这一层只做参数归一化与缓冲区搬运。
12
+ *
10
13
  * spec/plan 对应:plan Task 7;spec §6 API、§9 安全姿态。
11
14
  */
12
15
  import { fromBytes, toBytes } from '../runtime/convert.js';
@@ -42,6 +45,10 @@ function digestInput(input, label) {
42
45
  return new Uint8Array(0);
43
46
  return toBytes(input, label);
44
47
  }
48
+ /** `Uint8Array` 直接使用:紧接着就会 `set` 进 WASM 私有缓冲,再复制一份是纯浪费。 */
49
+ function inputBytes(input, label) {
50
+ return input instanceof Uint8Array ? input : digestInput(input, label);
51
+ }
45
52
  /**
46
53
  * SM3 摘要(同步)。
47
54
  *
@@ -49,7 +56,7 @@ function digestInput(input, label) {
49
56
  * @returns 32 字节摘要的 hex 字符串(64 字符)。
50
57
  */
51
58
  export function hash(input) {
52
- const bytes = digestInput(input, 'msg');
59
+ const bytes = inputBytes(input, 'msg');
53
60
  const { exports, memory } = getModule();
54
61
  const inputLen = bytes.length;
55
62
  // 从 WASM 堆分配缓冲区;**不可**用固定地址,否则覆盖 AS runtime 数据。
@@ -66,30 +73,30 @@ export function hash(input) {
66
73
  }
67
74
  }
68
75
  /**
69
- * HMAC-SM3(同步)。
76
+ * HMAC-SM3(同步,RFC 2104)。
70
77
  *
71
78
  * @param key 任意长度密钥。
72
79
  * @param msg 任意长度消息。
73
80
  * @returns 32 字节摘要的 hex 字符串。
74
81
  */
75
82
  export function hmac(key, msg) {
76
- const k = digestInput(key, 'key');
77
- const m = digestInput(msg, 'msg');
78
- const blockKey = k.length > 64 ? toBytes(hash(k), 'key64') : k;
79
- const padded = new Uint8Array(64);
80
- padded.set(blockKey);
81
- const innerKey = new Uint8Array(64);
82
- for (let i = 0; i < 64; i++)
83
- innerKey[i] = (padded[i] ?? 0) ^ 0x36;
84
- const outerKey = new Uint8Array(64);
85
- for (let i = 0; i < 64; i++)
86
- outerKey[i] = (padded[i] ?? 0) ^ 0x5c;
87
- const innerMsg = new Uint8Array(64 + m.length);
88
- innerMsg.set(innerKey, 0);
89
- innerMsg.set(m, 64);
90
- const inner = toBytes(hash(innerMsg), 'inner');
91
- const outerMsg = new Uint8Array(64 + 32);
92
- outerMsg.set(outerKey, 0);
93
- outerMsg.set(inner, 64);
94
- return hash(outerMsg);
83
+ const k = inputBytes(key, 'key');
84
+ const m = inputBytes(msg, 'msg');
85
+ const { exports, memory } = getModule();
86
+ // 空 key / 空 msg 也要占 1 字节:`alloc(0)` 返回的地址不保证可写。
87
+ const keyPtr = exports.alloc(k.length === 0 ? 1 : k.length);
88
+ const msgPtr = exports.alloc(m.length === 0 ? 1 : m.length);
89
+ const outPtr = exports.alloc(32);
90
+ try {
91
+ if (k.length > 0)
92
+ new Uint8Array(memory.buffer, keyPtr, k.length).set(k);
93
+ if (m.length > 0)
94
+ new Uint8Array(memory.buffer, msgPtr, m.length).set(m);
95
+ exports.sm3Hmac(keyPtr, k.length, msgPtr, m.length, outPtr);
96
+ const out = new Uint8Array(memory.buffer, outPtr, 32).slice();
97
+ return fromBytes(out, 'hex');
98
+ }
99
+ finally {
100
+ exports.freeAll();
101
+ }
95
102
  }
package/dist/sm3/sm3.wasm CHANGED
Binary file
@@ -0,0 +1,48 @@
1
+ /**
2
+ * SM9 WASM 模块的共享访问层。
3
+ *
4
+ * 把 `ensureLoaded` / `getModule` / 内存视图从业务 API 中拆出来,供
5
+ * `index.ts`、`kem.ts`、`hybrid.ts` 共用,避免模块间循环依赖。
6
+ *
7
+ * spec/plan 对应:`docs/superpowers/plans/2026-10-16-sm2-sm9-hybrid-encryption.md`。
8
+ */
9
+ /** SM9 曲线阶 n。 */
10
+ export declare const N: bigint;
11
+ /** WASM 导出的最小子集。诊断入口的子集。 */
12
+ export interface Sm9Exports extends WebAssembly.Exports {
13
+ readonly alloc: (n: number) => number;
14
+ readonly freeAll: () => void;
15
+ readonly signPubOut: (ksPtr: number, outPtr: number) => void;
16
+ readonly encPubOut: (kePtr: number, outPtr: number) => void;
17
+ readonly signDsOut: (ksPtr: number, idPtr: number, idLen: number, outPtr: number) => void;
18
+ readonly encDeOut: (kePtr: number, idPtr: number, idLen: number, outPtr: number) => void;
19
+ readonly sm9VerifyPubOut: (pubPtr: number, idPtr: number, idLen: number, msgPtr: number, msgLen: number, sigPtr: number) => number;
20
+ readonly sm9EncryptPubOut: (pubPtr: number, idPtr: number, idLen: number, msgPtr: number, msgLen: number, rPtr: number, outPtr: number, outCap: number) => number;
21
+ readonly sm9UserEncPubOut: (pubPtr: number, idPtr: number, idLen: number, outPtr: number) => void;
22
+ readonly sm9UserSignPubOut: (pubPtr: number, idPtr: number, idLen: number, outPtr: number) => void;
23
+ readonly sm9SignOut: (ksPtr: number, idPtr: number, idLen: number, msgPtr: number, msgLen: number, rPtr: number, outPtr: number) => void;
24
+ readonly sm9VerifyOut: (ksPtr: number, idPtr: number, idLen: number, msgPtr: number, msgLen: number, sigPtr: number) => number;
25
+ readonly sm9EncryptOut: (kePtr: number, idPtr: number, idLen: number, msgPtr: number, msgLen: number, rPtr: number, outPtr: number, outCap: number) => number;
26
+ readonly sm9DecryptOut: (kePtr: number, idPtr: number, idLen: number, ctPtr: number, ctLen: number, outPtr: number) => number;
27
+ readonly sm9KemEncryptOut: (kePtr: number, idPtr: number, idLen: number, rPtr: number, klen: number, c1Ptr: number, kPtr: number) => number;
28
+ /** 用加密主公钥(G1 仿射 64B)做 KEM 封装。 */
29
+ readonly sm9KemEncryptPubOut: (pubPtr: number, idPtr: number, idLen: number, rPtr: number, klen: number, c1Ptr: number, kPtr: number) => number;
30
+ readonly sm9KemDecryptOut: (kePtr: number, idPtr: number, idLen: number, c1Ptr: number, klen: number, kPtr: number) => number;
31
+ readonly sm9ExchEphemeralOut: (kePtr: number, peerIdPtr: number, peerIdLen: number, rPtr: number, outPtr: number) => void;
32
+ readonly sm9ExchKeyOut: (kePtr: number, isInitiator: number, selfIdPtr: number, selfIdLen: number, peerIdPtr: number, peerIdLen: number, rPtr: number, selfRPtr: number, peerRPtr: number, klen: number, outPtr: number) => number;
33
+ readonly sm9ExchConfirmOut: (kePtr: number, isInitiator: number, selfIdPtr: number, selfIdLen: number, peerIdPtr: number, peerIdLen: number, rPtr: number, selfRPtr: number, peerRPtr: number, prefix: number, outPtr: number) => void;
34
+ readonly sm9ExchAllOut: (kePtr: number, isInitiator: number, selfIdPtr: number, selfIdLen: number, peerIdPtr: number, peerIdLen: number, rPtr: number, selfRPtr: number, peerRPtr: number, klen: number, saPtr: number, sbPtr: number, kPtr: number) => number;
35
+ /** 诊断:cyclotomic 平方自测(随机子群元素上 cyc 平方 vs 通用平方)。1=一致。 */
36
+ readonly sqrCycSelfTestOut: (seed: number) => number;
37
+ readonly memory: WebAssembly.Memory;
38
+ }
39
+ export interface Sm9Module {
40
+ exports: Sm9Exports;
41
+ memory: WebAssembly.Memory;
42
+ }
43
+ /** 预热 WASM 模块。 */
44
+ export declare function ensureLoaded(): Promise<void>;
45
+ /** 取已加载的模块;未预热时抛错(调用方应先 `await ensureLoaded()`)。 */
46
+ export declare function getModule(): Sm9Module;
47
+ /** WASM 线性内存视图(每次现取,避免 memory.grow 后旧视图失效)。 */
48
+ export declare function mem(): Uint8Array;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * SM9 WASM 模块的共享访问层。
3
+ *
4
+ * 把 `ensureLoaded` / `getModule` / 内存视图从业务 API 中拆出来,供
5
+ * `index.ts`、`kem.ts`、`hybrid.ts` 共用,避免模块间循环依赖。
6
+ *
7
+ * spec/plan 对应:`docs/superpowers/plans/2026-10-16-sm2-sm9-hybrid-encryption.md`。
8
+ */
9
+ import { loadWasm } from '../runtime/loadWasm.js';
10
+ /** SM9 曲线阶 n。 */
11
+ export const N = BigInt('0xb640000002a3a6f1d603ab4ff58ec74449f2934b18ea8beee56ee19cd69ecf25');
12
+ /** 预热 WASM 模块。 */
13
+ export async function ensureLoaded() {
14
+ if (globalThis.__sm9Wasm)
15
+ return;
16
+ const mod = await loadWasm('sm9');
17
+ globalThis.__sm9Wasm = {
18
+ exports: mod.exports,
19
+ memory: mod.memory,
20
+ };
21
+ }
22
+ /** 取已加载的模块;未预热时抛错(调用方应先 `await ensureLoaded()`)。 */
23
+ export function getModule() {
24
+ const m = globalThis.__sm9Wasm;
25
+ if (!m)
26
+ throw new Error('sm9: 未初始化,请先 `await ensureLoaded()`。');
27
+ return m;
28
+ }
29
+ /** WASM 线性内存视图(每次现取,避免 memory.grow 后旧视图失效)。 */
30
+ export function mem() {
31
+ return new Uint8Array(getModule().memory.buffer);
32
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * SM9 混合加密(KEM + DEM):主公钥只保护一把短密钥,消息交给 SM4-GCM。
3
+ *
4
+ * 流程:
5
+ * ```
6
+ * { key, encapsulation } = KEM(masterPublicKey 或 masterPrivateKey, id, 16)
7
+ * { ct, tag } = SM4-GCM(key, iv, msg, aad = encapsulation)
8
+ * sealed = version || klen || encapLen || encapsulation
9
+ * || iv || tag || ct
10
+ * ```
11
+ *
12
+ * 与 `sm9.encrypt` 一致,第一参数既接受**加密主公钥**(64 字节 / SPKI PEM),
13
+ * 也接受**加密主私钥**(32 字节 / PKCS#8 PEM);前者是发送方的典型形态。
14
+ *
15
+ * 本模块同时用到 `sm9.wasm` 与 `sm4.wasm`:首次调用前请分别
16
+ * `await sm9.ensureLoaded()` 与 `await sm4.ensureLoaded()`。
17
+ *
18
+ * 完整性边界:SM9 KEM 的 encapsulation 只有 C1(65 字节),本身没有 C3/HMAC;
19
+ * 错误 id / 错误主公钥会得到不同的 K,最终由 GCM tag 认证失败暴露。因此
20
+ * `hybridDecrypt` 对错误 id 抛的是 GCM 的 `DecryptionError`,而不是 KEM 错误。
21
+ *
22
+ * spec/plan 对应:`docs/superpowers/plans/2026-10-16-sm2-sm9-hybrid-encryption.md`
23
+ * Task 4。
24
+ */
25
+ import { sealedFromObject, sealedToObject } from '../common/sealed.js';
26
+ export { sealedFromObject, sealedToObject };
27
+ /** `hybridEncrypt` 选项。 */
28
+ export interface Sm9HybridEncryptOptions {
29
+ /**
30
+ * 显式指定 GCM IV(12 字节 hex / Uint8Array)。
31
+ *
32
+ * 默认由宿主 CSPRNG 生成。**显式复用同一个 IV 配不同消息会破坏 GCM 的机密性**,
33
+ * 该参数只用于测试 / 协议互操作;正常调用不要传。
34
+ */
35
+ iv?: string | Uint8Array;
36
+ }
37
+ /** `hybridDecrypt` 选项。 */
38
+ export interface Sm9HybridDecryptOptions {
39
+ /**
40
+ * 解密失败时返回 `null` 而不抛错。
41
+ *
42
+ * 只覆盖密文支错误(`CiphertextError`:sealed 结构错、GCM tag 校验失败);
43
+ * 参数类型错误 / 密钥格式错误仍会抛出。
44
+ */
45
+ safeMode?: boolean;
46
+ }
47
+ /**
48
+ * SM9 混合加密。
49
+ *
50
+ * @param key 加密主公钥(64 字节 / SPKI PEM)或加密主私钥(32 字节 / PKCS#8 PEM)。
51
+ * @param id 接收方标识。
52
+ * @param message 明文(hex / Uint8Array;空消息传 `new Uint8Array(0)`)。
53
+ * @param opts `iv` 可选,默认随机 12 字节。
54
+ * @returns sealed hex(版本化,与 SM2 hybrid 同一布局)。
55
+ */
56
+ export declare function hybridEncrypt(key: string | Uint8Array, id: string | Uint8Array, message: string | Uint8Array, opts?: Sm9HybridEncryptOptions): string;
57
+ /**
58
+ * SM9 混合解密。
59
+ *
60
+ * @param ke 加密主私钥(32 字节 / PKCS#8 PEM)。
61
+ * @param id 接收方标识;必须与封装时一致。
62
+ * @param sealed `hybridEncrypt` 的返回值。
63
+ * @param opts `safeMode`:失败时返回 `null`。
64
+ * @returns 明文 hex。
65
+ */
66
+ export declare function hybridDecrypt(ke: string | Uint8Array, id: string | Uint8Array, sealed: string | Uint8Array, opts: Sm9HybridDecryptOptions & {
67
+ safeMode: true;
68
+ }): string | null;
69
+ export declare function hybridDecrypt(ke: string | Uint8Array, id: string | Uint8Array, sealed: string | Uint8Array, opts?: Sm9HybridDecryptOptions): string;