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 +110 -8
- package/dist/common/hybrid.d.ts +25 -0
- package/dist/common/hybrid.js +48 -0
- package/dist/common/sealed.d.ts +76 -0
- package/dist/common/sealed.js +160 -0
- package/dist/runtime/base64.core.d.ts +22 -0
- package/dist/runtime/base64.core.js +68 -0
- package/dist/runtime/convert.d.ts +12 -3
- package/dist/runtime/convert.js +31 -12
- package/dist/sm2/hybrid.d.ts +68 -0
- package/dist/sm2/hybrid.js +78 -0
- package/dist/sm2/index.d.ts +4 -0
- package/dist/sm2/index.js +5 -0
- package/dist/sm2/kem.d.ts +58 -0
- package/dist/sm2/kem.js +59 -0
- package/dist/sm9/core.d.ts +48 -0
- package/dist/sm9/core.js +32 -0
- package/dist/sm9/hybrid.d.ts +69 -0
- package/dist/sm9/hybrid.js +80 -0
- package/dist/sm9/index.d.ts +6 -26
- package/dist/sm9/index.js +11 -212
- package/dist/sm9/input.d.ts +43 -0
- package/dist/sm9/input.js +121 -0
- package/dist/sm9/kem.d.ts +48 -0
- package/dist/sm9/kem.js +123 -0
- package/dist/sm9/sm9.wasm +0 -0
- package/package.json +2 -1
package/dist/runtime/convert.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* spec/plan 对应:plan Task 2;spec §6 API 默认 hex、§9 安全姿态。
|
|
18
18
|
*/
|
|
19
|
+
import { decodeBase64, encodeBase64 } from './base64.core.js';
|
|
19
20
|
/**
|
|
20
21
|
* 把任意 `Input` 归一化为 `Uint8Array`。
|
|
21
22
|
*
|
|
@@ -106,20 +107,32 @@ export function hexToBytes(hex, label = 'hex') {
|
|
|
106
107
|
// tryParseHex 已覆盖全部合法情形,走到这里说明上面漏判,兜底抛错。
|
|
107
108
|
throw new TypeError(`hexToBytes(${label}): invalid hex`);
|
|
108
109
|
}
|
|
109
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* bytes → base64。优先级:Node `Buffer` → 宿主 `btoa` → 纯 JS 兜底。
|
|
112
|
+
*
|
|
113
|
+
* 微信小程序既没有 `Buffer` 也没有 `btoa`,会走最后一级;不需要宿主再安装
|
|
114
|
+
* base64 polyfill。
|
|
115
|
+
*/
|
|
110
116
|
export function bytesToBase64(bytes) {
|
|
111
117
|
if (HAS_BUFFER) {
|
|
112
118
|
return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString('base64');
|
|
113
119
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
if (HAS_BTOA) {
|
|
121
|
+
let bin = '';
|
|
122
|
+
for (let i = 0; i < bytes.length; i++)
|
|
123
|
+
bin += String.fromCharCode(bytes[i] ?? 0);
|
|
124
|
+
return btoa(bin);
|
|
125
|
+
}
|
|
126
|
+
return encodeBase64(bytes);
|
|
118
127
|
}
|
|
119
128
|
/** base64 合法字符集(含结尾 padding)。 */
|
|
120
129
|
const B64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
121
|
-
/**
|
|
122
|
-
|
|
130
|
+
/**
|
|
131
|
+
* base64 字符串 → bytes。失败抛 TypeError。
|
|
132
|
+
*
|
|
133
|
+
* 优先级:Node `Buffer` → 宿主 `atob` → 纯 JS 兜底(微信小程序等)。
|
|
134
|
+
*/
|
|
135
|
+
export function base64ToBytes(b64, label = 'base64') {
|
|
123
136
|
// Node 快路径。`Buffer.from(..., 'base64')` 对非法字符是静默丢弃的,
|
|
124
137
|
// 先用正则挡掉,非法的落到 `atob` 去抛错,保持与浏览器一致的行为。
|
|
125
138
|
if (HAS_BUFFER && B64_RE.test(b64)) {
|
|
@@ -128,11 +141,14 @@ export function base64ToBytes(b64, _label = 'base64') {
|
|
|
128
141
|
out.set(buf);
|
|
129
142
|
return out;
|
|
130
143
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
144
|
+
if (HAS_ATOB) {
|
|
145
|
+
const bin = atob(b64);
|
|
146
|
+
const out = new Uint8Array(bin.length);
|
|
147
|
+
for (let i = 0; i < bin.length; i++)
|
|
148
|
+
out[i] = bin.charCodeAt(i);
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
return decodeBase64(b64, label);
|
|
136
152
|
}
|
|
137
153
|
/** PEM → bytes。剥去 BEGIN/END armor,base64 解码。 */
|
|
138
154
|
export function pemToBytes(pem, label = 'pem') {
|
|
@@ -169,6 +185,9 @@ export function bytesToPem(bytes, kind) {
|
|
|
169
185
|
* (实测 4 KB 输入:hex 编码 52.6 µs → 1.9 µs)。
|
|
170
186
|
*/
|
|
171
187
|
const HAS_BUFFER = typeof Buffer !== 'undefined' && typeof Buffer.from === 'function';
|
|
188
|
+
/** 宿主是否提供 `btoa` / `atob`(现代浏览器有;微信小程序没有)。 */
|
|
189
|
+
const HAS_BTOA = typeof btoa === 'function';
|
|
190
|
+
const HAS_ATOB = typeof atob === 'function';
|
|
172
191
|
/** 16 进制数字查表(浏览器路径)。 */
|
|
173
192
|
const HEX_CHARS = '0123456789abcdef';
|
|
174
193
|
/**
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SM2 混合加密(KEM + DEM):公钥只保护一把短密钥,消息交给 SM4-GCM。
|
|
3
|
+
*
|
|
4
|
+
* 流程:
|
|
5
|
+
* ```
|
|
6
|
+
* DEK = CSPRNG(16) // 每次调用新生成
|
|
7
|
+
* { key, encapsulation } = kemEncrypt(publicKey, 16) // SM2 加密 DEK
|
|
8
|
+
* { ct, tag } = SM4-GCM(key, iv, msg, aad = encapsulation)
|
|
9
|
+
* sealed = version || klen || encapLen || encapsulation
|
|
10
|
+
* || iv || tag || ct
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* 安全要点(实现时逐条满足):
|
|
14
|
+
* - **每次调用都新生成 DEK 与 IV**,不跨调用复用;
|
|
15
|
+
* - IV 默认由宿主 CSPRNG 生成 12 字节;
|
|
16
|
+
* - SM2 的 C3 校验先于 GCM 解密,封装被篡改会在 KEM 阶段失败;
|
|
17
|
+
* - AAD 绑定 encapsulation,防止把一份封装配到另一份密文上;
|
|
18
|
+
* - sealed 首字节带版本号,未知版本拒绝解析。
|
|
19
|
+
*
|
|
20
|
+
* `hybridEncrypt` / `hybridDecrypt` 是同步函数(`.wasm` 由
|
|
21
|
+
* `await Promise.all([sm2.ensureLoaded(), sm4.ensureLoaded()])` 预热)。
|
|
22
|
+
*
|
|
23
|
+
* spec/plan 对应:`docs/superpowers/plans/2026-10-16-sm2-sm9-hybrid-encryption.md`
|
|
24
|
+
* Task 2 / Task 3。
|
|
25
|
+
*/
|
|
26
|
+
import { sealedFromObject, sealedToObject } from '../common/sealed.js';
|
|
27
|
+
export { sealedFromObject, sealedToObject };
|
|
28
|
+
/** `hybridEncrypt` 选项。 */
|
|
29
|
+
export interface Sm2HybridEncryptOptions {
|
|
30
|
+
/**
|
|
31
|
+
* 显式指定 GCM IV(12 字节 hex / Uint8Array)。
|
|
32
|
+
*
|
|
33
|
+
* 默认由宿主 CSPRNG 生成。**显式复用同一个 IV 配不同消息会破坏 GCM 的机密性**,
|
|
34
|
+
* 该参数只用于测试 / 协议互操作;正常调用不要传。
|
|
35
|
+
*/
|
|
36
|
+
iv?: string | Uint8Array;
|
|
37
|
+
}
|
|
38
|
+
/** `hybridDecrypt` 选项。 */
|
|
39
|
+
export interface Sm2HybridDecryptOptions {
|
|
40
|
+
/**
|
|
41
|
+
* 解密失败时返回 `null` 而不抛错。
|
|
42
|
+
*
|
|
43
|
+
* 只覆盖密文支错误(`CiphertextError`:sealed 结构错、KEM C3 校验失败、
|
|
44
|
+
* GCM tag 校验失败);参数类型错误仍会抛出。
|
|
45
|
+
*/
|
|
46
|
+
safeMode?: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* SM2 混合加密。
|
|
50
|
+
*
|
|
51
|
+
* @param publicKey 64 或 65 字节公钥(hex / Uint8Array / PEM)。
|
|
52
|
+
* @param message 明文(hex / Uint8Array;空消息传 `new Uint8Array(0)`)。
|
|
53
|
+
* @param opts `iv` 可选,默认随机 12 字节。
|
|
54
|
+
* @returns sealed hex(版本化,见模块注释)。
|
|
55
|
+
*/
|
|
56
|
+
export declare function hybridEncrypt(publicKey: string | Uint8Array, message: string | Uint8Array, opts?: Sm2HybridEncryptOptions): string;
|
|
57
|
+
/**
|
|
58
|
+
* SM2 混合解密。
|
|
59
|
+
*
|
|
60
|
+
* @param privateKey 32 字节私钥(hex / Uint8Array / PEM)。
|
|
61
|
+
* @param sealed `hybridEncrypt` 的返回值。
|
|
62
|
+
* @param opts `safeMode`:失败时返回 `null`。
|
|
63
|
+
* @returns 明文 hex。
|
|
64
|
+
*/
|
|
65
|
+
export declare function hybridDecrypt(privateKey: string | Uint8Array, sealed: string | Uint8Array, opts: Sm2HybridDecryptOptions & {
|
|
66
|
+
safeMode: true;
|
|
67
|
+
}): string | null;
|
|
68
|
+
export declare function hybridDecrypt(privateKey: string | Uint8Array, sealed: string | Uint8Array, opts?: Sm2HybridDecryptOptions): string;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SM2 混合加密(KEM + DEM):公钥只保护一把短密钥,消息交给 SM4-GCM。
|
|
3
|
+
*
|
|
4
|
+
* 流程:
|
|
5
|
+
* ```
|
|
6
|
+
* DEK = CSPRNG(16) // 每次调用新生成
|
|
7
|
+
* { key, encapsulation } = kemEncrypt(publicKey, 16) // SM2 加密 DEK
|
|
8
|
+
* { ct, tag } = SM4-GCM(key, iv, msg, aad = encapsulation)
|
|
9
|
+
* sealed = version || klen || encapLen || encapsulation
|
|
10
|
+
* || iv || tag || ct
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* 安全要点(实现时逐条满足):
|
|
14
|
+
* - **每次调用都新生成 DEK 与 IV**,不跨调用复用;
|
|
15
|
+
* - IV 默认由宿主 CSPRNG 生成 12 字节;
|
|
16
|
+
* - SM2 的 C3 校验先于 GCM 解密,封装被篡改会在 KEM 阶段失败;
|
|
17
|
+
* - AAD 绑定 encapsulation,防止把一份封装配到另一份密文上;
|
|
18
|
+
* - sealed 首字节带版本号,未知版本拒绝解析。
|
|
19
|
+
*
|
|
20
|
+
* `hybridEncrypt` / `hybridDecrypt` 是同步函数(`.wasm` 由
|
|
21
|
+
* `await Promise.all([sm2.ensureLoaded(), sm4.ensureLoaded()])` 预热)。
|
|
22
|
+
*
|
|
23
|
+
* spec/plan 对应:`docs/superpowers/plans/2026-10-16-sm2-sm9-hybrid-encryption.md`
|
|
24
|
+
* Task 2 / Task 3。
|
|
25
|
+
*/
|
|
26
|
+
import { resolveIv, toMessageBytes } from '../common/hybrid.js';
|
|
27
|
+
import { DEM_KEY_LEN, decodeSealed, encodeSealed, SEALED_VERSION, sealedFromObject, sealedToObject, } from '../common/sealed.js';
|
|
28
|
+
import { bytesToHex, hexToBytes } from '../runtime/convert.js';
|
|
29
|
+
import { CiphertextError, InvalidCiphertextError } from '../runtime/errors.js';
|
|
30
|
+
import { decryptGCM, encryptGCM } from '../sm4/index.js';
|
|
31
|
+
import { kemDecrypt, kemEncrypt } from './kem.js';
|
|
32
|
+
export { sealedFromObject, sealedToObject };
|
|
33
|
+
/**
|
|
34
|
+
* SM2 混合加密。
|
|
35
|
+
*
|
|
36
|
+
* @param publicKey 64 或 65 字节公钥(hex / Uint8Array / PEM)。
|
|
37
|
+
* @param message 明文(hex / Uint8Array;空消息传 `new Uint8Array(0)`)。
|
|
38
|
+
* @param opts `iv` 可选,默认随机 12 字节。
|
|
39
|
+
* @returns sealed hex(版本化,见模块注释)。
|
|
40
|
+
*/
|
|
41
|
+
export function hybridEncrypt(publicKey, message, opts = {}) {
|
|
42
|
+
const iv = resolveIv(opts.iv);
|
|
43
|
+
// 先校验 message,避免非法输入还白耗一次 SM2 公钥运算与随机数。
|
|
44
|
+
const msg = toMessageBytes(message, 'message');
|
|
45
|
+
// DEK 来自宿主 CSPRNG,每次调用都新生成;长度固定为 SM4-128 的 16 字节。
|
|
46
|
+
const { key, encapsulation } = kemEncrypt(publicKey, DEM_KEY_LEN);
|
|
47
|
+
const enc = hexToBytes(encapsulation, 'encapsulation');
|
|
48
|
+
// AAD 绑定 KEM 封装段:换掉封装就无法通过 GCM 认证。
|
|
49
|
+
const { ct, tag } = encryptGCM(key, iv, msg, enc);
|
|
50
|
+
return bytesToHex(encodeSealed({
|
|
51
|
+
version: SEALED_VERSION,
|
|
52
|
+
klen: DEM_KEY_LEN,
|
|
53
|
+
encapsulation: enc,
|
|
54
|
+
iv,
|
|
55
|
+
tag: hexToBytes(tag, 'tag'),
|
|
56
|
+
ct: ct.length === 0 ? new Uint8Array(0) : hexToBytes(ct, 'ct'),
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
export function hybridDecrypt(privateKey, sealed, opts = {}) {
|
|
60
|
+
try {
|
|
61
|
+
const parts = decodeSealed(sealed);
|
|
62
|
+
if (parts.klen !== DEM_KEY_LEN) {
|
|
63
|
+
throw new InvalidCiphertextError(`sm2.hybridDecrypt: sealed.klen=${parts.klen},当前版本只支持 ${DEM_KEY_LEN}`);
|
|
64
|
+
}
|
|
65
|
+
// SM2 KEM 的 encapsulation 是完整 SM2 密文:C1(65) + C3(32) + C2(klen)。
|
|
66
|
+
if (parts.encapsulation.length !== 97 + parts.klen) {
|
|
67
|
+
throw new InvalidCiphertextError(`sm2.hybridDecrypt: encapsulation 长度 ${parts.encapsulation.length} 与 klen=${parts.klen} 不符`);
|
|
68
|
+
}
|
|
69
|
+
// KEM 解封装先校验 SM2 C3;失败会抛 DecryptionError,不会进入 GCM。
|
|
70
|
+
const key = kemDecrypt(privateKey, parts.encapsulation, parts.klen);
|
|
71
|
+
return decryptGCM(key, parts.iv, parts.ct, parts.tag, { aad: parts.encapsulation });
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
if (opts.safeMode && err instanceof CiphertextError)
|
|
75
|
+
return null;
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
package/dist/sm2/index.d.ts
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 公钥)。
|
|
@@ -149,3 +151,5 @@ export declare function decrypt(privateKey: string | Uint8Array, ciphertext: str
|
|
|
149
151
|
}): string | null;
|
|
150
152
|
export declare function decrypt(privateKey: string | Uint8Array, ciphertext: string | Uint8Array, opts?: Sm2DecryptOptions): string;
|
|
151
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 公钥)。
|
|
@@ -478,3 +480,6 @@ function decryptRaw(d, ct) {
|
|
|
478
480
|
// SM2 密钥交换协议(GM/T 0003.3-2014):createExchangeInitiator /
|
|
479
481
|
// createExchangeResponder 由 `./exchange` 提供,在此并入 sm2 命名空间。
|
|
480
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;
|
package/dist/sm2/kem.js
ADDED
|
@@ -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
|
+
}
|
|
@@ -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;
|
package/dist/sm9/core.js
ADDED
|
@@ -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;
|
|
@@ -0,0 +1,80 @@
|
|
|
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 { resolveIv, toMessageBytes } from '../common/hybrid.js';
|
|
26
|
+
import { DEM_KEY_LEN, decodeSealed, encodeSealed, SEALED_VERSION, sealedFromObject, sealedToObject, } from '../common/sealed.js';
|
|
27
|
+
import { bytesToHex, hexToBytes } from '../runtime/convert.js';
|
|
28
|
+
import { CiphertextError, InvalidCiphertextError } from '../runtime/errors.js';
|
|
29
|
+
import { decryptGCM, encryptGCM } from '../sm4/index.js';
|
|
30
|
+
import { keyArg, toBytes } from './input.js';
|
|
31
|
+
import { kemDecrypt, kemEncapsulateWithMasterPublic, kemEncrypt } from './kem.js';
|
|
32
|
+
export { sealedFromObject, sealedToObject };
|
|
33
|
+
/**
|
|
34
|
+
* SM9 混合加密。
|
|
35
|
+
*
|
|
36
|
+
* @param key 加密主公钥(64 字节 / SPKI PEM)或加密主私钥(32 字节 / PKCS#8 PEM)。
|
|
37
|
+
* @param id 接收方标识。
|
|
38
|
+
* @param message 明文(hex / Uint8Array;空消息传 `new Uint8Array(0)`)。
|
|
39
|
+
* @param opts `iv` 可选,默认随机 12 字节。
|
|
40
|
+
* @returns sealed hex(版本化,与 SM2 hybrid 同一布局)。
|
|
41
|
+
*/
|
|
42
|
+
export function hybridEncrypt(key, id, message, opts = {}) {
|
|
43
|
+
const k = keyArg(key, 64, 'key');
|
|
44
|
+
const idB = toBytes(id, 'id');
|
|
45
|
+
const msg = toMessageBytes(message, 'message');
|
|
46
|
+
const iv = resolveIv(opts.iv);
|
|
47
|
+
// DEK 固定为 SM4-128 的 16 字节;封装方不需要 KGC 的加密主私钥。
|
|
48
|
+
const kem = k.kind === 'public'
|
|
49
|
+
? kemEncapsulateWithMasterPublic(k.bytes, idB, DEM_KEY_LEN)
|
|
50
|
+
: kemEncrypt(k.bytes, idB, DEM_KEY_LEN);
|
|
51
|
+
const enc = hexToBytes(kem.encapsulation, 'encapsulation');
|
|
52
|
+
const { ct, tag } = encryptGCM(kem.key, iv, msg, enc);
|
|
53
|
+
return bytesToHex(encodeSealed({
|
|
54
|
+
version: SEALED_VERSION,
|
|
55
|
+
klen: DEM_KEY_LEN,
|
|
56
|
+
encapsulation: enc,
|
|
57
|
+
iv,
|
|
58
|
+
tag: hexToBytes(tag, 'tag'),
|
|
59
|
+
ct: ct.length === 0 ? new Uint8Array(0) : hexToBytes(ct, 'ct'),
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
export function hybridDecrypt(ke, id, sealed, opts = {}) {
|
|
63
|
+
try {
|
|
64
|
+
const parts = decodeSealed(sealed);
|
|
65
|
+
if (parts.klen !== DEM_KEY_LEN) {
|
|
66
|
+
throw new InvalidCiphertextError(`sm9.hybridDecrypt: sealed.klen=${parts.klen},当前版本只支持 ${DEM_KEY_LEN}`);
|
|
67
|
+
}
|
|
68
|
+
if (parts.encapsulation.length !== 65) {
|
|
69
|
+
throw new InvalidCiphertextError(`sm9.hybridDecrypt: encapsulation 必须是 65 字节,实际 ${parts.encapsulation.length}`);
|
|
70
|
+
}
|
|
71
|
+
// KEM 只算 K;错误 id 不会在这里报错,由下面的 GCM tag 兜底。
|
|
72
|
+
const key = kemDecrypt(ke, id, parts.encapsulation, parts.klen);
|
|
73
|
+
return decryptGCM(key, parts.iv, parts.ct, parts.tag, { aad: parts.encapsulation });
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
if (opts.safeMode && err instanceof CiphertextError)
|
|
77
|
+
return null;
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
}
|