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.
- package/README.md +104 -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/sm2/hybrid.d.ts +68 -0
- package/dist/sm2/hybrid.js +78 -0
- package/dist/sm2/index.d.ts +45 -1
- package/dist/sm2/index.js +101 -6
- package/dist/sm2/kem.d.ts +58 -0
- package/dist/sm2/kem.js +59 -0
- package/dist/sm2/sm2.wasm +0 -0
- package/dist/sm3/index.d.ts +4 -1
- package/dist/sm3/index.js +28 -21
- package/dist/sm3/sm3.wasm +0 -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 +3 -1
|
@@ -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
|
+
}
|
package/dist/sm9/index.d.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* - `verify(ks, id, msg, sig)`:SM9 验签,通过返回 true
|
|
10
10
|
* - `encrypt(ke, id, msg, r?)`:SM9 加密,输出 C1||C3||C2 hex(C1=65B, C3=32B, C2=|msg|)
|
|
11
11
|
* - `decrypt(ke, id, ct)`:SM9 解密,输出明文 hex;C3 HMAC 校验失败抛错
|
|
12
|
+
* - `kemEncrypt` / `kemDecrypt`:KEM(GM/T 0044.4,见 `./kem.ts`)
|
|
13
|
+
* - `hybridEncrypt` / `hybridDecrypt`:KEM + SM4-GCM 混合加密(见 `./hybrid.ts`)
|
|
12
14
|
*
|
|
13
15
|
* 输入/输出 hex 全部不带 0x 前缀。
|
|
14
16
|
* r 不传时由宿主 CSPRNG 提供(Node: crypto.randomBytes / 浏览器: crypto.getRandomValues)。
|
|
@@ -22,8 +24,8 @@
|
|
|
22
24
|
*
|
|
23
25
|
* spec/plan 对应:plan Task 10;spec §6 API、§12.1 Q2(同步优先)、Q3(PKCS#8 + SM9 OID)。
|
|
24
26
|
*/
|
|
25
|
-
|
|
26
|
-
export
|
|
27
|
+
import { ensureLoaded } from './core.js';
|
|
28
|
+
export { ensureLoaded };
|
|
27
29
|
/** SM9 主密钥用途。 */
|
|
28
30
|
export type Sm9KeyPurpose = 'sign' | 'enc';
|
|
29
31
|
/** `generateKeyPair` 选项。 */
|
|
@@ -125,30 +127,7 @@ export declare function decrypt(ke: string | Uint8Array, id: string | Uint8Array
|
|
|
125
127
|
safeMode: true;
|
|
126
128
|
}): string | null;
|
|
127
129
|
export declare function decrypt(ke: string | Uint8Array, id: string | Uint8Array, ct: string | Uint8Array, opts?: Sm9DecryptOptions): string;
|
|
128
|
-
|
|
129
|
-
export interface KemResult {
|
|
130
|
-
/** 会话密钥 K(hex,长度为 `2*klen`)。 */
|
|
131
|
-
key: string;
|
|
132
|
-
/** 封装点 C1(65 字节未压缩 G1 点 hex:`04 || x || y`)。 */
|
|
133
|
-
encapsulation: string;
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* SM9 密钥封装(GM/T 0044.4):用接收方 id 与其加密主公钥 [ke]P1 封装一个会话密钥。
|
|
137
|
-
*
|
|
138
|
-
* @param ke 32 字节加密主私钥 hex(用于派生主公钥 [ke]P1)
|
|
139
|
-
* @param id 接收方标识
|
|
140
|
-
* @param klen 会话密钥字节长度(默认 32)
|
|
141
|
-
* @param r 32 字节随机数 hex(可选;不传则由宿主 CSPRNG 提供)
|
|
142
|
-
* @returns `{ key, encapsulation }`
|
|
143
|
-
*/
|
|
144
|
-
export declare function kemEncrypt(ke: string | Uint8Array, id: string | Uint8Array, klen?: number, r?: string | Uint8Array): KemResult;
|
|
145
|
-
/**
|
|
146
|
-
* SM9 密钥解封装:用本端加密主私钥 ke 与 id 从封装点 C1 恢复会话密钥。
|
|
147
|
-
*
|
|
148
|
-
* @param encapsulation 65 字节封装点 C1 hex
|
|
149
|
-
* @returns 会话密钥 hex
|
|
150
|
-
*/
|
|
151
|
-
export declare function kemDecrypt(ke: string | Uint8Array, id: string | Uint8Array, encapsulation: string | Uint8Array, klen?: number): string;
|
|
130
|
+
export { type KemResult, kemDecrypt, kemEncrypt } from './kem.js';
|
|
152
131
|
/** 密钥交换端点配置。 */
|
|
153
132
|
export interface Sm9ExchangeOptions {
|
|
154
133
|
/** 会话密钥字节长度(默认 32)。 */
|
|
@@ -193,3 +172,4 @@ export declare function createExchangeInitiator(ke: string | Uint8Array, selfId:
|
|
|
193
172
|
* @param peerId 对端(A)标识 ID_A
|
|
194
173
|
*/
|
|
195
174
|
export declare function createExchangeResponder(ke: string | Uint8Array, selfId: string | Uint8Array, peerId: string | Uint8Array, options?: Sm9ExchangeOptions): Promise<Sm9KeyExchangeResponder>;
|
|
175
|
+
export * from './hybrid.js';
|
package/dist/sm9/index.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* - `verify(ks, id, msg, sig)`:SM9 验签,通过返回 true
|
|
10
10
|
* - `encrypt(ke, id, msg, r?)`:SM9 加密,输出 C1||C3||C2 hex(C1=65B, C3=32B, C2=|msg|)
|
|
11
11
|
* - `decrypt(ke, id, ct)`:SM9 解密,输出明文 hex;C3 HMAC 校验失败抛错
|
|
12
|
+
* - `kemEncrypt` / `kemDecrypt`:KEM(GM/T 0044.4,见 `./kem.ts`)
|
|
13
|
+
* - `hybridEncrypt` / `hybridDecrypt`:KEM + SM4-GCM 混合加密(见 `./hybrid.ts`)
|
|
12
14
|
*
|
|
13
15
|
* 输入/输出 hex 全部不带 0x 前缀。
|
|
14
16
|
* r 不传时由宿主 CSPRNG 提供(Node: crypto.randomBytes / 浏览器: crypto.getRandomValues)。
|
|
@@ -22,152 +24,12 @@
|
|
|
22
24
|
*
|
|
23
25
|
* spec/plan 对应:plan Task 10;spec §6 API、§12.1 Q2(同步优先)、Q3(PKCS#8 + SM9 OID)。
|
|
24
26
|
*/
|
|
25
|
-
import { encodeSm9PrivateKeyPem, encodeSm9PublicKeyPem
|
|
27
|
+
import { encodeSm9PrivateKeyPem, encodeSm9PublicKeyPem } from '../pem/sm9.js';
|
|
26
28
|
import { bytesToHex, hexToBytes } from '../runtime/convert.js';
|
|
27
|
-
import { CiphertextError, DecryptionError, InvalidCiphertextError
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
const N = BigInt('0xb640000002a3a6f1d603ab4ff58ec74449f2934b18ea8beee56ee19cd69ecf25');
|
|
32
|
-
/** 预热 WASM 模块。 */
|
|
33
|
-
export async function ensureLoaded() {
|
|
34
|
-
if (globalThis.__sm9Wasm)
|
|
35
|
-
return;
|
|
36
|
-
const mod = await loadWasm('sm9');
|
|
37
|
-
globalThis.__sm9Wasm = {
|
|
38
|
-
exports: mod.exports,
|
|
39
|
-
memory: mod.memory,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
function getModule() {
|
|
43
|
-
const m = globalThis.__sm9Wasm;
|
|
44
|
-
if (!m)
|
|
45
|
-
throw new Error('sm9: 未初始化,请先 `await ensureLoaded()`。');
|
|
46
|
-
return m;
|
|
47
|
-
}
|
|
48
|
-
// ---------------- 工具 ----------------
|
|
49
|
-
/** 任意输入 → Uint8Array。
|
|
50
|
-
* - Uint8Array: 拷一份
|
|
51
|
-
* - string 且看起来是 hex(偶数长度 + 全部 0-9a-fA-F):按 hex 解
|
|
52
|
-
* - 其他 string:按 UTF-8 编码(用于 id、msg 等人类可读输入)
|
|
53
|
-
* 失败抛 TypeError。 */
|
|
54
|
-
/**
|
|
55
|
-
* 通用输入归一化:Uint8Array 拷贝、hex 解码、其它按 UTF-8。
|
|
56
|
-
*
|
|
57
|
-
* **刻意不做 PEM 嗅探** —— PEM 只可能出现在密钥参数上,由 `masterPrivateBytes`
|
|
58
|
-
* 显式处理;这样 id / msg 里出现 `-----BEGIN` 也不会被误吞。
|
|
59
|
-
*/
|
|
60
|
-
function toBytes(input, label) {
|
|
61
|
-
if (input instanceof Uint8Array)
|
|
62
|
-
return new Uint8Array(input);
|
|
63
|
-
if (typeof input !== 'string')
|
|
64
|
-
throw new TypeError(`sm9: ${label} 必须是字符串或 Uint8Array`);
|
|
65
|
-
if (input.startsWith('0x') || /^[0-9a-fA-F]+$/.test(input)) {
|
|
66
|
-
const s = input.startsWith('0x') ? input.slice(2) : input;
|
|
67
|
-
if (s.length % 2 !== 0)
|
|
68
|
-
throw new TypeError(`sm9: ${label} hex 长度必须为偶数`);
|
|
69
|
-
return hexToBytes(s, label);
|
|
70
|
-
}
|
|
71
|
-
return new TextEncoder().encode(input);
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* 主私钥参数归一化:接受 32 字节(hex / Uint8Array)或 PKCS#8 私钥 PEM
|
|
75
|
-
* (spec §12.1 Q3,算法 OID `1.2.156.10197.1.100`)。
|
|
76
|
-
*/
|
|
77
|
-
function masterPrivateBytes(input, label) {
|
|
78
|
-
let bytes;
|
|
79
|
-
if (typeof input === 'string' && input.includes('-----BEGIN')) {
|
|
80
|
-
const parsed = parseSm9PrivateKeyPem(input);
|
|
81
|
-
if (parsed.length !== 32) {
|
|
82
|
-
throw new InvalidKeyError(`sm9: ${label} 作为 PEM 主私钥应为 32 字节,实际 ${parsed.length}`);
|
|
83
|
-
}
|
|
84
|
-
bytes = parsed;
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
const b = toBytes(input, label);
|
|
88
|
-
if (b.length !== 32) {
|
|
89
|
-
throw new InvalidKeyError(`sm9: ${label} 必须是 32 字节主私钥,实际 ${b.length}`);
|
|
90
|
-
}
|
|
91
|
-
bytes = b;
|
|
92
|
-
}
|
|
93
|
-
// 标量范围:SM9(GM/T 0044)要求 1 ≤ k ≤ n−1。
|
|
94
|
-
// 越界值不会崩,但 wasm 侧的 addModN → modAdd 只做一次条件减、要求输入已
|
|
95
|
-
// 约化(h1 + k < 2n),k ≥ n 时 k + h1 可能 ≥ 2n 而静默产出错值,故在此显式拒绝。
|
|
96
|
-
const k = BigInt(`0x${bytesToHex(bytes)}`);
|
|
97
|
-
if (k < 1n || k >= N) {
|
|
98
|
-
throw new InvalidKeyError(`sm9: ${label} 必须在 [1, n−1] 范围内(SM9 主私钥标量)`);
|
|
99
|
-
}
|
|
100
|
-
return bytes;
|
|
101
|
-
}
|
|
102
|
-
/** 主公钥参数归一化:64(加密 G1)或 128(签名 G2)字节点,或 SPKI 主公钥 PEM。 */
|
|
103
|
-
function masterPublicBytes(input, label) {
|
|
104
|
-
if (typeof input === 'string' && input.includes('-----BEGIN')) {
|
|
105
|
-
const pub = parseSm9PublicKeyPem(input);
|
|
106
|
-
if (pub.length !== 64 && pub.length !== 128) {
|
|
107
|
-
throw new InvalidKeyError(`sm9: ${label} 作为 PEM 主公钥必须是 64 或 128 字节,实际 ${pub.length}`);
|
|
108
|
-
}
|
|
109
|
-
return pub;
|
|
110
|
-
}
|
|
111
|
-
const b = toBytes(input, label);
|
|
112
|
-
if (b.length !== 64 && b.length !== 128) {
|
|
113
|
-
throw new InvalidKeyError(`sm9: ${label} 必须是 64(加密)或 128(签名)字节主公钥,实际 ${b.length}`);
|
|
114
|
-
}
|
|
115
|
-
return b;
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* 解析 `verify` / `encrypt` 的密钥参数:主私钥(32 字节 / PKCS#8 私钥 PEM)
|
|
119
|
-
* 或主公钥(`publicLen` 字节点 / SPKI 公钥 PEM)。
|
|
120
|
-
*
|
|
121
|
-
* 两者的长度区间不重叠(主私钥恒为 32 字节),因此按长度即可判别。
|
|
122
|
-
*/
|
|
123
|
-
function keyArg(input, publicLen, label) {
|
|
124
|
-
if (typeof input === 'string' && input.includes('-----BEGIN')) {
|
|
125
|
-
if (input.includes('BEGIN PUBLIC KEY')) {
|
|
126
|
-
const pub = parseSm9PublicKeyPem(input);
|
|
127
|
-
if (pub.length !== publicLen) {
|
|
128
|
-
throw new InvalidKeyError(`sm9: ${label} 作为 PEM 主公钥应为 ${publicLen} 字节,实际 ${pub.length}`);
|
|
129
|
-
}
|
|
130
|
-
return { kind: 'public', bytes: pub };
|
|
131
|
-
}
|
|
132
|
-
return { kind: 'secret', bytes: masterPrivateBytes(input, label) };
|
|
133
|
-
}
|
|
134
|
-
const b = toBytes(input, label);
|
|
135
|
-
if (b.length === 32)
|
|
136
|
-
return { kind: 'secret', bytes: b };
|
|
137
|
-
if (b.length === publicLen)
|
|
138
|
-
return { kind: 'public', bytes: b };
|
|
139
|
-
throw new InvalidKeyError(`sm9: ${label} 必须是 32 字节主私钥或 ${publicLen} 字节主公钥,实际 ${b.length}`);
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* 校验字节长度。默认按**密钥支**(`InvalidKeyError`)抛错 —— SM9 各入口的参数绝大多数
|
|
143
|
-
* 是主私钥 / 标量;密文、签名、协议消息的调用点显式传 `'cipher'`。
|
|
144
|
-
*/
|
|
145
|
-
function requireLen(b, n, label, kind = 'key') {
|
|
146
|
-
if (b.length === n)
|
|
147
|
-
return;
|
|
148
|
-
const msg = `sm9: ${label} 必须是 ${n} 字节,实际 ${b.length}`;
|
|
149
|
-
if (kind === 'cipher')
|
|
150
|
-
throw new InvalidCiphertextError(msg);
|
|
151
|
-
throw new InvalidKeyError(msg);
|
|
152
|
-
}
|
|
153
|
-
function mem() {
|
|
154
|
-
return new Uint8Array(getModule().memory.buffer);
|
|
155
|
-
}
|
|
156
|
-
function _allocBytes(n) {
|
|
157
|
-
return getModule().exports.alloc(n);
|
|
158
|
-
}
|
|
159
|
-
function putHex(hex, ptr) {
|
|
160
|
-
mem().set(hexToBytes(hex), ptr);
|
|
161
|
-
}
|
|
162
|
-
function putBytes(b, ptr) {
|
|
163
|
-
mem().set(b, ptr);
|
|
164
|
-
}
|
|
165
|
-
function readHex(ptr, len) {
|
|
166
|
-
return bytesToHex(mem().subarray(ptr, ptr + len));
|
|
167
|
-
}
|
|
168
|
-
function randomRHex() {
|
|
169
|
-
return randomScalarBelow(N).toString(16).padStart(64, '0');
|
|
170
|
-
}
|
|
29
|
+
import { CiphertextError, DecryptionError, InvalidCiphertextError } from '../runtime/errors.js';
|
|
30
|
+
import { ensureLoaded, getModule, mem } from './core.js';
|
|
31
|
+
import { keyArg, masterPrivateBytes, masterPublicBytes, putBytes, putHex, randomRHex, readHex, requireLen, toBytes, } from './input.js';
|
|
32
|
+
export { ensureLoaded };
|
|
171
33
|
/**
|
|
172
34
|
* 生成 SM9 主密钥对与对应用户私钥(spec §6、§12.1 Q3)。
|
|
173
35
|
*
|
|
@@ -443,73 +305,8 @@ function decryptUnsafe(ke, id, ct) {
|
|
|
443
305
|
exports.freeAll();
|
|
444
306
|
}
|
|
445
307
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
*
|
|
449
|
-
* @param ke 32 字节加密主私钥 hex(用于派生主公钥 [ke]P1)
|
|
450
|
-
* @param id 接收方标识
|
|
451
|
-
* @param klen 会话密钥字节长度(默认 32)
|
|
452
|
-
* @param r 32 字节随机数 hex(可选;不传则由宿主 CSPRNG 提供)
|
|
453
|
-
* @returns `{ key, encapsulation }`
|
|
454
|
-
*/
|
|
455
|
-
export function kemEncrypt(ke, id, klen = 32, r) {
|
|
456
|
-
const keB = masterPrivateBytes(ke, 'ke');
|
|
457
|
-
const idB = toBytes(id, 'id');
|
|
458
|
-
const rB = r !== undefined ? toBytes(r, 'r') : toBytes(randomRHex(), 'r');
|
|
459
|
-
requireLen(rB, 32, 'r');
|
|
460
|
-
if (!Number.isInteger(klen) || klen <= 0)
|
|
461
|
-
throw new RangeError('sm9: klen 必须为正整数');
|
|
462
|
-
const { exports } = getModule();
|
|
463
|
-
const keP = exports.alloc(32);
|
|
464
|
-
const idP = exports.alloc(idB.length);
|
|
465
|
-
const rP = exports.alloc(32);
|
|
466
|
-
const c1P = exports.alloc(65);
|
|
467
|
-
const kP = exports.alloc(klen);
|
|
468
|
-
try {
|
|
469
|
-
putBytes(keB, keP);
|
|
470
|
-
putBytes(idB, idP);
|
|
471
|
-
putBytes(rB, rP);
|
|
472
|
-
const rc = exports.sm9KemEncryptOut(keP, idP, idB.length, rP, klen, c1P, kP);
|
|
473
|
-
if (rc !== 1)
|
|
474
|
-
throw new Error('sm9: kemEncrypt 失败(K 全 0,需换随机数重试)');
|
|
475
|
-
return { key: readHex(kP, klen), encapsulation: readHex(c1P, 65) };
|
|
476
|
-
}
|
|
477
|
-
finally {
|
|
478
|
-
exports.freeAll();
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
/**
|
|
482
|
-
* SM9 密钥解封装:用本端加密主私钥 ke 与 id 从封装点 C1 恢复会话密钥。
|
|
483
|
-
*
|
|
484
|
-
* @param encapsulation 65 字节封装点 C1 hex
|
|
485
|
-
* @returns 会话密钥 hex
|
|
486
|
-
*/
|
|
487
|
-
export function kemDecrypt(ke, id, encapsulation, klen = 32) {
|
|
488
|
-
const keB = masterPrivateBytes(ke, 'ke');
|
|
489
|
-
const idB = toBytes(id, 'id');
|
|
490
|
-
const c1B = toBytes(encapsulation, 'encapsulation');
|
|
491
|
-
if (c1B.length !== 65)
|
|
492
|
-
throw new RangeError(`sm9: encapsulation 必须是 65 字节,实际 ${c1B.length}`);
|
|
493
|
-
if (!Number.isInteger(klen) || klen <= 0)
|
|
494
|
-
throw new RangeError('sm9: klen 必须为正整数');
|
|
495
|
-
const { exports } = getModule();
|
|
496
|
-
const keP = exports.alloc(32);
|
|
497
|
-
const idP = exports.alloc(idB.length);
|
|
498
|
-
const c1P = exports.alloc(65);
|
|
499
|
-
const kP = exports.alloc(klen);
|
|
500
|
-
try {
|
|
501
|
-
putBytes(keB, keP);
|
|
502
|
-
putBytes(idB, idP);
|
|
503
|
-
putBytes(c1B, c1P);
|
|
504
|
-
const rc = exports.sm9KemDecryptOut(keP, idP, idB.length, c1P, klen, kP);
|
|
505
|
-
if (rc !== 1)
|
|
506
|
-
throw new DecryptionError('sm9: kemDecrypt 失败(封装非法 / K 全 0)');
|
|
507
|
-
return readHex(kP, klen);
|
|
508
|
-
}
|
|
509
|
-
finally {
|
|
510
|
-
exports.freeAll();
|
|
511
|
-
}
|
|
512
|
-
}
|
|
308
|
+
// KEM 实现拆到 ./kem.ts,供 hybrid.ts 复用且避免循环依赖。
|
|
309
|
+
export { kemDecrypt, kemEncrypt } from './kem.js';
|
|
513
310
|
function ctEq(a, b) {
|
|
514
311
|
if (a.length !== b.length)
|
|
515
312
|
return false;
|
|
@@ -678,3 +475,5 @@ export async function createExchangeResponder(ke, selfId, peerId, options = {})
|
|
|
678
475
|
},
|
|
679
476
|
};
|
|
680
477
|
}
|
|
478
|
+
// SM9 混合加密(KEM + SM4-GCM)。
|
|
479
|
+
export * from './hybrid.js';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SM9 输入归一化与密钥参数校验。
|
|
3
|
+
*
|
|
4
|
+
* 从 `index.ts` 拆出,供 `kem.ts` / `hybrid.ts` 复用,避免循环依赖。
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* 通用输入归一化:Uint8Array 拷贝、hex 解码、其它按 UTF-8。
|
|
8
|
+
*
|
|
9
|
+
* **刻意不做 PEM 嗅探** —— PEM 只可能出现在密钥参数上,由 `masterPrivateBytes`
|
|
10
|
+
* 显式处理;这样 id / msg 里出现 `-----BEGIN` 也不会被误吞。
|
|
11
|
+
*/
|
|
12
|
+
export declare function toBytes(input: string | Uint8Array, label: string): Uint8Array;
|
|
13
|
+
/**
|
|
14
|
+
* 主私钥参数归一化:接受 32 字节(hex / Uint8Array)或 PKCS#8 私钥 PEM
|
|
15
|
+
* (spec §12.1 Q3,算法 OID `1.2.156.10197.1.100`)。
|
|
16
|
+
*/
|
|
17
|
+
export declare function masterPrivateBytes(input: string | Uint8Array, label: string): Uint8Array;
|
|
18
|
+
/** 主公钥参数归一化:64(加密 G1)或 128(签名 G2)字节点,或 SPKI 主公钥 PEM。 */
|
|
19
|
+
export declare function masterPublicBytes(input: string | Uint8Array, label: string): Uint8Array;
|
|
20
|
+
/** `verify` / `encrypt` 第一参数的解析结果。 */
|
|
21
|
+
export type KeyArg = {
|
|
22
|
+
kind: 'secret';
|
|
23
|
+
bytes: Uint8Array;
|
|
24
|
+
} | {
|
|
25
|
+
kind: 'public';
|
|
26
|
+
bytes: Uint8Array;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* 解析 `verify` / `encrypt` 的密钥参数:主私钥(32 字节 / PKCS#8 私钥 PEM)
|
|
30
|
+
* 或主公钥(`publicLen` 字节点 / SPKI 公钥 PEM)。
|
|
31
|
+
*
|
|
32
|
+
* 两者的长度区间不重叠(主私钥恒为 32 字节),因此按长度即可判别。
|
|
33
|
+
*/
|
|
34
|
+
export declare function keyArg(input: string | Uint8Array, publicLen: 64 | 128, label: string): KeyArg;
|
|
35
|
+
/**
|
|
36
|
+
* 校验字节长度。默认按**密钥支**(`InvalidKeyError`)抛错 —— SM9 各入口的参数绝大多数
|
|
37
|
+
* 是主私钥 / 标量;密文、签名、协议消息的调用点显式传 `'cipher'`。
|
|
38
|
+
*/
|
|
39
|
+
export declare function requireLen(b: Uint8Array, n: number, label: string, kind?: 'key' | 'cipher'): void;
|
|
40
|
+
export declare function putHex(hex: string, ptr: number): void;
|
|
41
|
+
export declare function putBytes(b: Uint8Array, ptr: number): void;
|
|
42
|
+
export declare function readHex(ptr: number, len: number): string;
|
|
43
|
+
export declare function randomRHex(): string;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SM9 输入归一化与密钥参数校验。
|
|
3
|
+
*
|
|
4
|
+
* 从 `index.ts` 拆出,供 `kem.ts` / `hybrid.ts` 复用,避免循环依赖。
|
|
5
|
+
*/
|
|
6
|
+
import { parseSm9PrivateKeyPem, parseSm9PublicKeyPem } from '../pem/sm9.js';
|
|
7
|
+
import { bytesToHex, hexToBytes } from '../runtime/convert.js';
|
|
8
|
+
import { InvalidCiphertextError, InvalidKeyError } from '../runtime/errors.js';
|
|
9
|
+
import { randomScalarBelow } from '../runtime/random.js';
|
|
10
|
+
import { mem, N } from './core.js';
|
|
11
|
+
/**
|
|
12
|
+
* 通用输入归一化:Uint8Array 拷贝、hex 解码、其它按 UTF-8。
|
|
13
|
+
*
|
|
14
|
+
* **刻意不做 PEM 嗅探** —— PEM 只可能出现在密钥参数上,由 `masterPrivateBytes`
|
|
15
|
+
* 显式处理;这样 id / msg 里出现 `-----BEGIN` 也不会被误吞。
|
|
16
|
+
*/
|
|
17
|
+
export function toBytes(input, label) {
|
|
18
|
+
if (input instanceof Uint8Array)
|
|
19
|
+
return new Uint8Array(input);
|
|
20
|
+
if (typeof input !== 'string')
|
|
21
|
+
throw new TypeError(`sm9: ${label} 必须是字符串或 Uint8Array`);
|
|
22
|
+
if (input.startsWith('0x') || /^[0-9a-fA-F]+$/.test(input)) {
|
|
23
|
+
const s = input.startsWith('0x') ? input.slice(2) : input;
|
|
24
|
+
if (s.length % 2 !== 0)
|
|
25
|
+
throw new TypeError(`sm9: ${label} hex 长度必须为偶数`);
|
|
26
|
+
return hexToBytes(s, label);
|
|
27
|
+
}
|
|
28
|
+
return new TextEncoder().encode(input);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 主私钥参数归一化:接受 32 字节(hex / Uint8Array)或 PKCS#8 私钥 PEM
|
|
32
|
+
* (spec §12.1 Q3,算法 OID `1.2.156.10197.1.100`)。
|
|
33
|
+
*/
|
|
34
|
+
export function masterPrivateBytes(input, label) {
|
|
35
|
+
let bytes;
|
|
36
|
+
if (typeof input === 'string' && input.includes('-----BEGIN')) {
|
|
37
|
+
const parsed = parseSm9PrivateKeyPem(input);
|
|
38
|
+
if (parsed.length !== 32) {
|
|
39
|
+
throw new InvalidKeyError(`sm9: ${label} 作为 PEM 主私钥应为 32 字节,实际 ${parsed.length}`);
|
|
40
|
+
}
|
|
41
|
+
bytes = parsed;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const b = toBytes(input, label);
|
|
45
|
+
if (b.length !== 32) {
|
|
46
|
+
throw new InvalidKeyError(`sm9: ${label} 必须是 32 字节主私钥,实际 ${b.length}`);
|
|
47
|
+
}
|
|
48
|
+
bytes = b;
|
|
49
|
+
}
|
|
50
|
+
// 标量范围:SM9(GM/T 0044)要求 1 ≤ k ≤ n−1。
|
|
51
|
+
// 越界值不会崩,但 wasm 侧的 addModN → modAdd 只做一次条件减、要求输入已
|
|
52
|
+
// 约化(h1 + k < 2n),k ≥ n 时 k + h1 可能 ≥ 2n 而静默产出错值,故在此显式拒绝。
|
|
53
|
+
const k = BigInt(`0x${bytesToHex(bytes)}`);
|
|
54
|
+
if (k < 1n || k >= N) {
|
|
55
|
+
throw new InvalidKeyError(`sm9: ${label} 必须在 [1, n−1] 范围内(SM9 主私钥标量)`);
|
|
56
|
+
}
|
|
57
|
+
return bytes;
|
|
58
|
+
}
|
|
59
|
+
/** 主公钥参数归一化:64(加密 G1)或 128(签名 G2)字节点,或 SPKI 主公钥 PEM。 */
|
|
60
|
+
export function masterPublicBytes(input, label) {
|
|
61
|
+
if (typeof input === 'string' && input.includes('-----BEGIN')) {
|
|
62
|
+
const pub = parseSm9PublicKeyPem(input);
|
|
63
|
+
if (pub.length !== 64 && pub.length !== 128) {
|
|
64
|
+
throw new InvalidKeyError(`sm9: ${label} 作为 PEM 主公钥必须是 64 或 128 字节,实际 ${pub.length}`);
|
|
65
|
+
}
|
|
66
|
+
return pub;
|
|
67
|
+
}
|
|
68
|
+
const b = toBytes(input, label);
|
|
69
|
+
if (b.length !== 64 && b.length !== 128) {
|
|
70
|
+
throw new InvalidKeyError(`sm9: ${label} 必须是 64(加密)或 128(签名)字节主公钥,实际 ${b.length}`);
|
|
71
|
+
}
|
|
72
|
+
return b;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 解析 `verify` / `encrypt` 的密钥参数:主私钥(32 字节 / PKCS#8 私钥 PEM)
|
|
76
|
+
* 或主公钥(`publicLen` 字节点 / SPKI 公钥 PEM)。
|
|
77
|
+
*
|
|
78
|
+
* 两者的长度区间不重叠(主私钥恒为 32 字节),因此按长度即可判别。
|
|
79
|
+
*/
|
|
80
|
+
export function keyArg(input, publicLen, label) {
|
|
81
|
+
if (typeof input === 'string' && input.includes('-----BEGIN')) {
|
|
82
|
+
if (input.includes('BEGIN PUBLIC KEY')) {
|
|
83
|
+
const pub = parseSm9PublicKeyPem(input);
|
|
84
|
+
if (pub.length !== publicLen) {
|
|
85
|
+
throw new InvalidKeyError(`sm9: ${label} 作为 PEM 主公钥应为 ${publicLen} 字节,实际 ${pub.length}`);
|
|
86
|
+
}
|
|
87
|
+
return { kind: 'public', bytes: pub };
|
|
88
|
+
}
|
|
89
|
+
return { kind: 'secret', bytes: masterPrivateBytes(input, label) };
|
|
90
|
+
}
|
|
91
|
+
const b = toBytes(input, label);
|
|
92
|
+
if (b.length === 32)
|
|
93
|
+
return { kind: 'secret', bytes: b };
|
|
94
|
+
if (b.length === publicLen)
|
|
95
|
+
return { kind: 'public', bytes: b };
|
|
96
|
+
throw new InvalidKeyError(`sm9: ${label} 必须是 32 字节主私钥或 ${publicLen} 字节主公钥,实际 ${b.length}`);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* 校验字节长度。默认按**密钥支**(`InvalidKeyError`)抛错 —— SM9 各入口的参数绝大多数
|
|
100
|
+
* 是主私钥 / 标量;密文、签名、协议消息的调用点显式传 `'cipher'`。
|
|
101
|
+
*/
|
|
102
|
+
export function requireLen(b, n, label, kind = 'key') {
|
|
103
|
+
if (b.length === n)
|
|
104
|
+
return;
|
|
105
|
+
const msg = `sm9: ${label} 必须是 ${n} 字节,实际 ${b.length}`;
|
|
106
|
+
if (kind === 'cipher')
|
|
107
|
+
throw new InvalidCiphertextError(msg);
|
|
108
|
+
throw new InvalidKeyError(msg);
|
|
109
|
+
}
|
|
110
|
+
export function putHex(hex, ptr) {
|
|
111
|
+
mem().set(hexToBytes(hex), ptr);
|
|
112
|
+
}
|
|
113
|
+
export function putBytes(b, ptr) {
|
|
114
|
+
mem().set(b, ptr);
|
|
115
|
+
}
|
|
116
|
+
export function readHex(ptr, len) {
|
|
117
|
+
return bytesToHex(mem().subarray(ptr, ptr + len));
|
|
118
|
+
}
|
|
119
|
+
export function randomRHex() {
|
|
120
|
+
return randomScalarBelow(N).toString(16).padStart(64, '0');
|
|
121
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SM9 密钥封装(KEM,GM/T 0044.4)。
|
|
3
|
+
*
|
|
4
|
+
* 两种封装入口:
|
|
5
|
+
* - `kemEncrypt(ke, id, klen, r?)`:用加密主私钥派生 Ppub-e(既有已发布接口)。
|
|
6
|
+
* - `kemEncapsulateWithMasterPublic(pub, id, klen, r?)`:直接用加密主公钥
|
|
7
|
+
* `[ke]P1`(发送方的典型形态;供 `hybrid.ts` 使用,不改变既有 `kemEncrypt`)。
|
|
8
|
+
*
|
|
9
|
+
* 解封装只有 `kemDecrypt(ke, id, encapsulation, klen)`:必须持有加密主私钥。
|
|
10
|
+
*
|
|
11
|
+
* spec/plan 对应:`docs/superpowers/plans/2026-10-16-sm2-sm9-hybrid-encryption.md`
|
|
12
|
+
* Task 4。
|
|
13
|
+
*/
|
|
14
|
+
/** KEM 封装结果。 */
|
|
15
|
+
export interface KemResult {
|
|
16
|
+
/** 会话密钥 K(hex,长度为 `2*klen`)。 */
|
|
17
|
+
key: string;
|
|
18
|
+
/** 封装点 C1(65 字节未压缩 G1 点 hex:`04 || x || y`)。 */
|
|
19
|
+
encapsulation: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* SM9 密钥封装(GM/T 0044.4):用接收方 id 与其加密主公钥 [ke]P1 封装一个会话密钥。
|
|
23
|
+
*
|
|
24
|
+
* @param ke 32 字节加密主私钥 hex(用于派生主公钥 [ke]P1)
|
|
25
|
+
* @param id 接收方标识
|
|
26
|
+
* @param klen 会话密钥字节长度(默认 32)
|
|
27
|
+
* @param r 32 字节随机数 hex(可选;不传则由宿主 CSPRNG 提供)
|
|
28
|
+
* @returns `{ key, encapsulation }`
|
|
29
|
+
*/
|
|
30
|
+
export declare function kemEncrypt(ke: string | Uint8Array, id: string | Uint8Array, klen?: number, r?: string | Uint8Array): KemResult;
|
|
31
|
+
/**
|
|
32
|
+
* 用加密主公钥封装的内部入口(`hybridEncrypt` 使用)。
|
|
33
|
+
*
|
|
34
|
+
* 与 `kemEncrypt` 的区别只在密钥参数:这里不需要 KGC 的加密主私钥。
|
|
35
|
+
*
|
|
36
|
+
* @param masterPublicKey 64 字节加密主公钥 `[ke]P1`(点字节 / SPKI 主公钥 PEM)。
|
|
37
|
+
* @param id 接收方标识。
|
|
38
|
+
* @param klen 会话密钥字节长度(默认 32)。
|
|
39
|
+
* @param r 32 字节随机数 hex(可选)。
|
|
40
|
+
*/
|
|
41
|
+
export declare function kemEncapsulateWithMasterPublic(masterPublicKey: string | Uint8Array, id: string | Uint8Array, klen?: number, r?: string | Uint8Array): KemResult;
|
|
42
|
+
/**
|
|
43
|
+
* SM9 密钥解封装:用本端加密主私钥 ke 与 id 从封装点 C1 恢复会话密钥。
|
|
44
|
+
*
|
|
45
|
+
* @param encapsulation 65 字节封装点 C1 hex
|
|
46
|
+
* @returns 会话密钥 hex
|
|
47
|
+
*/
|
|
48
|
+
export declare function kemDecrypt(ke: string | Uint8Array, id: string | Uint8Array, encapsulation: string | Uint8Array, klen?: number): string;
|