sm-crypto-wasm 0.1.2 → 0.2.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/dist/sm2/index.d.ts +41 -1
- package/dist/sm2/index.js +96 -6
- 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/sm9.wasm +0 -0
- package/package.json +3 -1
package/dist/sm2/index.d.ts
CHANGED
|
@@ -70,14 +70,54 @@ export interface Sm2DecryptOptions {
|
|
|
70
70
|
* @returns PKCS#8 私钥 PEM 与 SPKI 公钥 PEM。
|
|
71
71
|
*/
|
|
72
72
|
export declare function generateKeyPair(): Promise<Sm2KeyPair>;
|
|
73
|
+
/**
|
|
74
|
+
* 池中的一个预计算签名点。
|
|
75
|
+
*
|
|
76
|
+
* `point`(`k·G` 的坐标)是公开信息;**`k` 被封在模块内部**,外部读不到,被
|
|
77
|
+
* `sign()` 取走后立刻从索引中摘除并抹掉字节 —— 所以"复用同一个 `k`"在 API 层面
|
|
78
|
+
* 就不可能发生,调用方不需要(也无法)自己去保证这一点。
|
|
79
|
+
*/
|
|
80
|
+
export declare class SignaturePoint {
|
|
81
|
+
/** `k·G` 的仿射坐标 `x ‖ y`(64 字节 hex)。公开信息,可用于审计。 */
|
|
82
|
+
readonly point: string;
|
|
83
|
+
/** 只允许本模块的 `createSignaturePointPool` 构造。 */
|
|
84
|
+
private constructor();
|
|
85
|
+
/** @internal 仅供 `createSignaturePointPool` 使用。 */
|
|
86
|
+
static _create(nonce: Uint8Array, point: string): SignaturePoint;
|
|
87
|
+
}
|
|
88
|
+
/** `sign` 的选项。 */
|
|
89
|
+
export interface SignOptions {
|
|
90
|
+
/**
|
|
91
|
+
* 预计算的签名点池(见 `createSignaturePointPool`)。
|
|
92
|
+
*
|
|
93
|
+
* 命中时省掉本次签名的固定基点标量乘(实测占单次签名的 56%);池被取空后
|
|
94
|
+
* 自动回退到现算。**该数组会被原地消费**(每签一次 `pop()` 一个点),
|
|
95
|
+
* 调用方无需自己维护游标。
|
|
96
|
+
*/
|
|
97
|
+
pointPool?: SignaturePoint[];
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 预计算 `n` 个签名点,供 `sign()` 的 `pointPool` 选项消费。
|
|
101
|
+
*
|
|
102
|
+
* 每个点 = 一次 `k·G`(本机约 130 µs)。批量签名时把这些固定基点标量乘提前
|
|
103
|
+
* 摊销,签名的关键路径只剩 `e = SM3(Z ‖ M)` 与两条模运算 —— 实测快 5.9×。
|
|
104
|
+
*
|
|
105
|
+
* `k` 取自宿主 CSPRNG,并由本模块独占保管:调用方拿不到它,也就无从误用成复用。
|
|
106
|
+
* 唯一的约定是"同一个点只喂一次",而真喂两次会直接抛错。
|
|
107
|
+
*
|
|
108
|
+
* @param n 生成个数。
|
|
109
|
+
* @returns `n` 个签名点。
|
|
110
|
+
*/
|
|
111
|
+
export declare function createSignaturePointPool(n: number): Promise<SignaturePoint[]>;
|
|
73
112
|
/**
|
|
74
113
|
* SM2 签名(默认 SM3withSM2、默认用户标识)。
|
|
75
114
|
*
|
|
76
115
|
* @param privateKey 32 字节私钥。
|
|
77
116
|
* @param message 待签名消息。
|
|
117
|
+
* @param opts `pointPool`:预计算的签名点池(见 `createSignaturePointPool`)。
|
|
78
118
|
* @returns 64 字节签名(r || s)的 hex。
|
|
79
119
|
*/
|
|
80
|
-
export declare function sign(privateKey: string | Uint8Array, message: string | Uint8Array): string;
|
|
120
|
+
export declare function sign(privateKey: string | Uint8Array, message: string | Uint8Array, opts?: SignOptions): string;
|
|
81
121
|
/**
|
|
82
122
|
* SM2 验签。
|
|
83
123
|
*
|
package/dist/sm2/index.js
CHANGED
|
@@ -215,34 +215,122 @@ function randomK() {
|
|
|
215
215
|
}
|
|
216
216
|
return out;
|
|
217
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* 池中的一个预计算签名点。
|
|
220
|
+
*
|
|
221
|
+
* `point`(`k·G` 的坐标)是公开信息;**`k` 被封在模块内部**,外部读不到,被
|
|
222
|
+
* `sign()` 取走后立刻从索引中摘除并抹掉字节 —— 所以"复用同一个 `k`"在 API 层面
|
|
223
|
+
* 就不可能发生,调用方不需要(也无法)自己去保证这一点。
|
|
224
|
+
*/
|
|
225
|
+
export class SignaturePoint {
|
|
226
|
+
/** `k·G` 的仿射坐标 `x ‖ y`(64 字节 hex)。公开信息,可用于审计。 */
|
|
227
|
+
point;
|
|
228
|
+
/** 只允许本模块的 `createSignaturePointPool` 构造。 */
|
|
229
|
+
constructor(point) {
|
|
230
|
+
this.point = point;
|
|
231
|
+
}
|
|
232
|
+
/** @internal 仅供 `createSignaturePointPool` 使用。 */
|
|
233
|
+
static _create(nonce, point) {
|
|
234
|
+
const p = new SignaturePoint(point);
|
|
235
|
+
POINT_NONCE.set(p, nonce);
|
|
236
|
+
return p;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* 点位 → 一次性 `k`。取走即 `delete`:同一个点位对象无论被谁持有引用,都只能
|
|
241
|
+
* 取到一次 `k`。用 `WeakMap` 而非对象属性,是为了连"枚举键名"都做不到。
|
|
242
|
+
*/
|
|
243
|
+
const POINT_NONCE = new WeakMap();
|
|
244
|
+
/**
|
|
245
|
+
* 取走签名点的一次性 `k`。
|
|
246
|
+
*
|
|
247
|
+
* 索引里没有(已被取过,或不是本库产出的伪造对象)就抛错 —— 于是"同一个点签两次"
|
|
248
|
+
* 会当场失败,而不是悄悄复用 `k` 把私钥送出去。
|
|
249
|
+
*/
|
|
250
|
+
function takeNonce(p) {
|
|
251
|
+
const nonce = POINT_NONCE.get(p);
|
|
252
|
+
if (nonce === undefined) {
|
|
253
|
+
throw new Error('sm2: 该签名点不可用(已被使用过,或不是 createSignaturePointPool 产出的);签名点是一次性的。');
|
|
254
|
+
}
|
|
255
|
+
POINT_NONCE.delete(p);
|
|
256
|
+
return nonce;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* 预计算 `n` 个签名点,供 `sign()` 的 `pointPool` 选项消费。
|
|
260
|
+
*
|
|
261
|
+
* 每个点 = 一次 `k·G`(本机约 130 µs)。批量签名时把这些固定基点标量乘提前
|
|
262
|
+
* 摊销,签名的关键路径只剩 `e = SM3(Z ‖ M)` 与两条模运算 —— 实测快 5.9×。
|
|
263
|
+
*
|
|
264
|
+
* `k` 取自宿主 CSPRNG,并由本模块独占保管:调用方拿不到它,也就无从误用成复用。
|
|
265
|
+
* 唯一的约定是"同一个点只喂一次",而真喂两次会直接抛错。
|
|
266
|
+
*
|
|
267
|
+
* @param n 生成个数。
|
|
268
|
+
* @returns `n` 个签名点。
|
|
269
|
+
*/
|
|
270
|
+
export async function createSignaturePointPool(n) {
|
|
271
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
272
|
+
throw new RangeError(`sm2: 签名点池大小必须是非负整数,收到 ${String(n)}`);
|
|
273
|
+
}
|
|
274
|
+
await ensureLoaded();
|
|
275
|
+
const { exports, memory } = getModule();
|
|
276
|
+
const pool = [];
|
|
277
|
+
for (let i = 0; i < n; i++) {
|
|
278
|
+
const nonce = randomK();
|
|
279
|
+
const kPtr = exports.alloc(32);
|
|
280
|
+
const ptPtr = exports.alloc(64);
|
|
281
|
+
try {
|
|
282
|
+
new Uint8Array(memory.buffer, kPtr, 32).set(nonce);
|
|
283
|
+
exports.scalarMulGOut(kPtr, ptPtr);
|
|
284
|
+
pool.push(SignaturePoint._create(nonce, fromBytes(new Uint8Array(memory.buffer, ptPtr, 64).slice(), 'hex')));
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
exports.freeAll();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return pool;
|
|
291
|
+
}
|
|
218
292
|
/**
|
|
219
293
|
* SM2 签名(默认 SM3withSM2、默认用户标识)。
|
|
220
294
|
*
|
|
221
295
|
* @param privateKey 32 字节私钥。
|
|
222
296
|
* @param message 待签名消息。
|
|
297
|
+
* @param opts `pointPool`:预计算的签名点池(见 `createSignaturePointPool`)。
|
|
223
298
|
* @returns 64 字节签名(r || s)的 hex。
|
|
224
299
|
*/
|
|
225
|
-
export function sign(privateKey, message) {
|
|
300
|
+
export function sign(privateKey, message, opts = {}) {
|
|
226
301
|
const d = toPrivateKeyBytes(privateKey);
|
|
227
302
|
const m = toBytes(message, 'message');
|
|
228
303
|
const pub = derivePublicKeyBytes(d);
|
|
229
|
-
const { exports } = getModule();
|
|
304
|
+
const { exports, memory } = getModule();
|
|
230
305
|
const dp = exports.alloc(32);
|
|
231
306
|
const pxp = exports.alloc(32);
|
|
232
307
|
const pyp = exports.alloc(32);
|
|
233
308
|
const mp = exports.alloc(m.length);
|
|
234
309
|
const kp = exports.alloc(32);
|
|
310
|
+
const xp = exports.alloc(32);
|
|
235
311
|
const op = exports.alloc(64);
|
|
312
|
+
const pool = opts.pointPool;
|
|
236
313
|
try {
|
|
237
314
|
putBytes(d, dp);
|
|
238
315
|
putBytes(pub.subarray(0, 32), pxp);
|
|
239
316
|
putBytes(pub.subarray(32, 64), pyp);
|
|
240
317
|
putBytes(m, mp);
|
|
241
|
-
// 最多重试几次 k(极小概率 r/s 为 0 或 r+k==n
|
|
318
|
+
// 最多重试几次 k(极小概率 r/s 为 0 或 r+k==n)。池命中时每轮换一个点。
|
|
242
319
|
for (let attempt = 0; attempt < 16; attempt++) {
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
320
|
+
const pooled = pool?.pop();
|
|
321
|
+
let rc;
|
|
322
|
+
if (pooled !== undefined) {
|
|
323
|
+
// 一次性取走 k;此处之后该点不可再用(再喂会抛错)。
|
|
324
|
+
const nonce = takeNonce(pooled);
|
|
325
|
+
putBytes(nonce, kp);
|
|
326
|
+
nonce.fill(0); // JS 侧那份随即抹掉
|
|
327
|
+
new Uint8Array(memory.buffer, xp, 32).set(toBytes(pooled.point, 'point').subarray(0, 32));
|
|
328
|
+
rc = exports.signWithPointOut(dp, pxp, pyp, mp, m.length, kp, xp, op);
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
putBytes(randomK(), kp);
|
|
332
|
+
rc = exports.signOut(dp, pxp, pyp, mp, m.length, kp, op);
|
|
333
|
+
}
|
|
246
334
|
if (rc === 0) {
|
|
247
335
|
return fromBytes(readBytes(op, 64), 'hex');
|
|
248
336
|
}
|
|
@@ -250,6 +338,8 @@ export function sign(privateKey, message) {
|
|
|
250
338
|
throw new Error('sm2.sign: failed to produce a valid signature after retries');
|
|
251
339
|
}
|
|
252
340
|
finally {
|
|
341
|
+
// 抹掉 WASM 内存里那份 k(缓冲区在 freeAll 前仍可写)
|
|
342
|
+
new Uint8Array(memory.buffer, kp, 32).fill(0);
|
|
253
343
|
exports.freeAll();
|
|
254
344
|
}
|
|
255
345
|
}
|
package/dist/sm2/sm2.wasm
CHANGED
|
Binary file
|
package/dist/sm3/index.d.ts
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
|
/**
|
|
@@ -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 =
|
|
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 =
|
|
77
|
-
const m =
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
package/dist/sm9/sm9.wasm
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sm-crypto-wasm",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "国密 SM2/SM3/SM4/SM9/ZUC 的 WASM 实现,Node 18+ 与现代浏览器均可使用,按算法分包懒加载。",
|
|
5
5
|
"description:en": "SM2/SM3/SM4/SM9/ZUC (China crypto) implemented in AssemblyScript -> WASM. Node 18+ & modern browsers, per-algorithm lazy loading.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -92,6 +92,7 @@
|
|
|
92
92
|
"format": "biome format --write src tests bench",
|
|
93
93
|
"typecheck": "tsc --noEmit",
|
|
94
94
|
"bench": "node --import tsx bench/run.ts",
|
|
95
|
+
"bench:cubelrti": "node --import tsx bench/compare-cubelrti.bench.ts",
|
|
95
96
|
"docs:dev": "pnpm build && vitepress dev website",
|
|
96
97
|
"docs:build": "pnpm build && vitepress build website",
|
|
97
98
|
"docs:preview": "vitepress preview website",
|
|
@@ -106,6 +107,7 @@
|
|
|
106
107
|
"happy-dom": "21.0.1",
|
|
107
108
|
"mitata": "^1.0.34",
|
|
108
109
|
"release-it": "^21.1.1",
|
|
110
|
+
"sm-crypto-v2": "1.15.1",
|
|
109
111
|
"tsx": "^4.19.2",
|
|
110
112
|
"typescript": "^7.0.2",
|
|
111
113
|
"vitepress": "^1.6.4",
|