sm-crypto-wasm 0.1.0 → 0.1.2
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 +154 -418
- package/dist/esbuild-plugin.d.ts +70 -0
- package/dist/esbuild-plugin.js +85 -0
- package/dist/runtime/loadWasm.browser.d.ts +39 -0
- package/dist/runtime/loadWasm.browser.js +78 -0
- package/dist/runtime/loadWasm.core.d.ts +45 -0
- package/dist/runtime/loadWasm.core.js +70 -0
- package/dist/runtime/loadWasm.d.ts +16 -27
- package/dist/runtime/loadWasm.js +28 -95
- package/dist/runtime/random.browser.d.ts +25 -0
- package/dist/runtime/random.browser.js +50 -0
- package/dist/runtime/random.core.d.ts +20 -0
- package/dist/runtime/random.core.js +35 -0
- package/dist/runtime/random.d.ts +16 -4
- package/dist/runtime/random.js +33 -28
- package/dist/sm2/index.js +39 -25
- package/dist/sm4/index.js +2 -2
- package/dist/sm9/index.js +8 -4
- package/dist/sm9/sm9.wasm +0 -0
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -38,116 +38,13 @@ Web Workers). Algorithms are split into sub-packages and lazy-loaded on demand.
|
|
|
38
38
|
|
|
39
39
|
## 性能
|
|
40
40
|
|
|
41
|
-
热点(大整数模运算、hash
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
> SM3 / SM4 / SM9 / ZUC。表中 SM2 的数字取自同一次运行(`wasm` 与 `gmkitx` 两列同源);
|
|
49
|
-
> **本轮优化在 wasm 计算层的净收益**(更稳定,min-of-5 采样)见下方「要点」。
|
|
50
|
-
|
|
51
|
-
| 算法 | 操作 | sm-crypto-wasm | 纯 JS 参考(gmkitx) | 对比 |
|
|
52
|
-
|---|---|---|---|---|
|
|
53
|
-
| SM3 | hash 32 B / 1 KB / 64 KB | 1.04 µs / 6.27 µs / 330 µs | 13.7 µs(1 KB) | 快 2.2× |
|
|
54
|
-
| SM3 | hmac 1 KB | 10.1 µs | 17.9 µs | 快 1.8× |
|
|
55
|
-
| SM4 | ECB 16 B / 1 KB | 0.94 µs / 6.98 µs | 37.3 µs(1 KB) | 快 5.3× |
|
|
56
|
-
| SM4 | CBC 1 KB | 10.5 µs | 41.6 µs | 快 4.0× |
|
|
57
|
-
| SM4 | CTR 1 KB | 7.43 µs | 36.5 µs | 快 4.9× |
|
|
58
|
-
| SM4 | CFB 1 KB | 10.0 µs | 37.9 µs | 快 3.8× |
|
|
59
|
-
| SM4 | OFB 1 KB | 9.98 µs | 36.3 µs | 快 3.6× |
|
|
60
|
-
| SM4 | GCM 1 KB | 19.8 µs | 221 µs | 快 11.2× |
|
|
61
|
-
| SM2 | 密钥对生成 | 151 µs | — | — |
|
|
62
|
-
| SM2 | 公钥派生 | 110 µs | 197 µs | 快 1.8× |
|
|
63
|
-
| SM2 | 签名 64 B | 142 µs | 446 µs | 快 3.1× |
|
|
64
|
-
| SM2 | 验签 64 B | 402 µs | 2.85 ms | 快 7.1× |
|
|
65
|
-
| SM2 | 加密 64 B | 499 µs | 2.96 ms | 快 5.9× |
|
|
66
|
-
| SM2 | 解密 64 B | 343 µs | 2.59 ms | 快 7.6× |
|
|
67
|
-
| SM2 | 密钥交换(完整 A↔B) | 1.52 ms | 11.4 ms | 快 7.5× |
|
|
68
|
-
| SM9 | 主密钥对生成(签名 / 加密) | 2.07 / 1.73 ms | — | — |
|
|
69
|
-
| SM9 | 主公钥派生(签名 / 加密) | 97.1 / 49.1 µs | — | — |
|
|
70
|
-
| SM9 | 用户公钥派生 | 563 µs | — | — |
|
|
71
|
-
| SM9 | 签名 32 B | 4.11 ms | — | — |
|
|
72
|
-
| SM9 | 验签 32 B | 8.00 ms | — | — |
|
|
73
|
-
| SM9 | 加密 32 B | 3.69 ms | — | — |
|
|
74
|
-
| SM9 | 解密 32 B | 5.40 ms | — | — |
|
|
75
|
-
| SM9 | KEM 封装 / 解封装 | 3.42 / 4.92 ms | — | — |
|
|
76
|
-
| SM9 | 密钥交换(完整 A↔B) | 19.6 ms | — | — |
|
|
77
|
-
| ZUC | EEA3 加密 1 KB | 9.07 µs | 20.5 µs | 快 2.3× |
|
|
78
|
-
| ZUC | EEA3 密钥流 1 KB | 6.15 µs | — | — |
|
|
79
|
-
| ZUC | EIA3 MAC 1 KB | 11.2 µs | — | — |
|
|
80
|
-
|
|
81
|
-
要点:
|
|
82
|
-
|
|
83
|
-
- **所有经过对比的操作都快于纯 JS 参考**:SM3 / ZUC 快 1.8\~2.3×,SM4 六种模式
|
|
84
|
-
3.6\~11.2×,SM2 快 1.8\~7.6×;
|
|
85
|
-
- **SM4 的 CFB / OFB 曾比纯 JS 慢 1.4~1.5×**:这两种模式原先在 TS 侧逐块调用
|
|
86
|
-
wasm(每 16 字节一次 alloc + 调用 + 取值,1 KB 要 64 次跨界);补上与 CBC/CTR
|
|
87
|
-
同款的批量导出后降到 10 µs 一带,反过来快 3.6~3.8×;
|
|
88
|
-
- **SM4 的瓶颈是每轮 4 次 S 盒查表**:把 S 盒与 L 变换合成 T-table 只换来 8~10%
|
|
89
|
-
(查表次数不变),因为成本不在 ALU 而在内存访问。查表次数既然降不下来,就改用
|
|
90
|
-
指令级并行把它藏起来:ECB 的多块循环做成 **8 块标量交错**(每块 4 条独立依赖链,
|
|
91
|
-
由乱序执行盖住 L1 延迟),CTR / GCM 的密钥流生成同路数,不足 8 块时退到 4 块交错、
|
|
92
|
-
再退到单块。之所以不走 WASM SIMD:`v128` 既没有 AVX2 的 gather(无法并行查
|
|
93
|
-
8→32 位表),也没有 AES-NI,而 `i8x16.swizzle` 只够 4-bit S 盒、够不着 SM4 的
|
|
94
|
-
8-bit S 盒——查表次数不降,收益只剩异或链的 lane 并行,实测反而不如标量交错。
|
|
95
|
-
这也解释了为什么仍追不上 C:它靠 ARMv8 的 `SM4E` 指令用电路做 S 盒、**根本不查表**,
|
|
96
|
-
而 WebAssembly 不暴露该指令集(通用 SIMD 也只给通用算子);
|
|
97
|
-
- **SM9 没有可比实现**(纯 JS 生态普遍如此,pairing 在纯 JS 里成本过高),故只给
|
|
98
|
-
本库的绝对耗时;
|
|
99
|
-
- **SM2 的 wasm 大整数是线性内存裸指针**(`U256` 即 `usize`,配模块级 bump arena 与
|
|
100
|
-
每次导出调用回卷),在这一层之上做了八件事:
|
|
101
|
-
1. **素域 p 的归约走 Solinas**——p = 2^256−2^224−2^96+2^64−1 是伪 Mersenne,展开
|
|
102
|
-
2^256 ≡ 2^224+2^96−2^64+1 后归约只需加减移位、没有乘法(实测归约仅 0.03 µs,
|
|
103
|
-
比通用长除法快一个数量级);
|
|
104
|
-
2. **平方改 Comba(列扫描)+ 统一进位**——旧实现每对 (i,j) 要跑两遍完整的进位传播
|
|
105
|
-
循环(共 56 次内存循环),是整个库最贵的单个函数;
|
|
106
|
-
3. **标量乘分两条路**:固定基点走 comb 预计算表(w=5、32 项;51 次倍点 + 52 次点加),
|
|
107
|
-
任意点走 wNAF(w=5,点加从 ~128 次降到 ~43 次)。表惰性构造一次,缓存在 arena
|
|
108
|
-
之外的永久区;
|
|
109
|
-
4. **点运算全程原地**:倍点 / 点加写模块级 scratch,不再逐次 `new U256` + `new JPoint`
|
|
110
|
-
(一次倍点原本 ~11 次堆分配,而标量乘要调用它几百次);
|
|
111
|
-
5. **点加走混合坐标**——comb 表与 G 的 wNAF 表在构建时用一次 **Montgomery 批量求逆**
|
|
112
|
-
转成仿射(n 次求逆 → 1 次求逆 + 3n 次乘法,32 项表约 25 µs、且只做一次),于是
|
|
113
|
-
循环里每次点加由 12M+4S 降到 **8M+3S**(省约 1/3);
|
|
114
|
-
6. **验签改联合标量乘**——`s·G + t·P` 用两条共享同一串倍点的 wNAF(G 的 16 项预计算
|
|
115
|
-
表同样仿射缓存、走混合坐标),省掉 comb 那条路的 51 次倍点;
|
|
116
|
-
7. **求逆改局部字级二进制扩展欧几里得**——`u`、`v` 与两个 Bezout 系数全部放进 wasm
|
|
117
|
-
局部变量(256 位恰好 4 个 u64 装下),每轮零内存往返,单次求逆 12.8 µs → 2.3 µs
|
|
118
|
-
(SM2 与 SM9 共用这条路径,故 SM9 一并受益);
|
|
119
|
-
8. **`sign` 的公钥派生走进程内缓存**——公钥与 Z 值只依赖私钥,而原先每次签名都要
|
|
120
|
-
重做一遍固定基点标量乘(实测 105 µs,占一次签名 \~250 µs 的 41%);缓存以私钥的
|
|
121
|
-
hex 为键、上限 32 条 FIFO 淘汰,命中即跳过派生。实测 `sign` 252 → 142 µs。
|
|
122
|
-
第 1~7 项在 wasm 计算层、第 8 项在 TS 门面层:前七项的 A/B(min-of-5,同机、同 probe)
|
|
123
|
-
是标量乘 G 138 → 105 µs、求逆 12.8 → 2.3 µs、签名 156 → 122 µs、验签 474 → 424 µs、
|
|
124
|
-
加密 487 → 449 µs、密钥交换 563 → 511 µs;在此之前的历史累积(Solinas + Comba +
|
|
125
|
-
comb/wNAF + 原地化)已把签名从 6.10 ms 降下来;
|
|
126
|
-
**取舍**:公开的 `getPublicKey` 仍每次真算(保持"派生公钥"的语义与基准可测性),
|
|
127
|
-
而缓存让"私钥的派生结果"(以及私钥本身的 hex 形态)在进程内存里驻留 —— 不接受
|
|
128
|
-
这一点的场景用 `configurePublicKeyCache({ enabled: false })` 关掉,即回到每次重算。
|
|
129
|
-
`encrypt` 只收公钥、不涉及派生,门面开销未动;
|
|
130
|
-
- **代价与取舍**:comb 只适用于固定基点,Solinas 只适用于伪 Mersenne 模数——SM2 的
|
|
131
|
-
标量模 n 与 SM9 的 p/n 都不是这个形状,SM9 的基域因而改走 Montgomery(见下条);
|
|
132
|
-
**混合坐标**要求表项是
|
|
133
|
-
仿射,所以每张预计算表都要先付一次批量求逆——G 的两张表缓存在永久区、只摊一次,而
|
|
134
|
-
任意点的 wNAF 表每次现算,批量求逆与节省的点加基本抵消,因此仍留 Jacobian;
|
|
135
|
-
**联合标量乘**只在 `s·G + t·P` 这种「一个固定基点 + 一个任意点」的组合上成立,
|
|
136
|
-
单点标量乘用不上;
|
|
137
|
-
- SM9 由 pairing 主导。基域 Fp 已改用 **Montgomery 域**(CIOS,R = 2²⁵⁶):SM9 的 p 是
|
|
138
|
-
BN 参数、不是伪 Mersenne,原先一次模乘要付 Barrett 的 64 次部分积 + 约 153 次归约
|
|
139
|
-
乘法,CIOS 只要 2×64 次;元素在域内一律是 a·R,只在 wasm 边界(字节 ↔ 元素、模块
|
|
140
|
-
常量)转换一次。单次模乘 179 → 98 ns(1.8×),端到端 sign / verify / encrypt /
|
|
141
|
-
decrypt / KEM / 密钥交换全面快 **1.6~1.8×**;扩域元素(Fp2/Fp4/Fp12)改用连续内存
|
|
142
|
-
表示则让 sign / verify 相对更早的版本快约 2.6×;
|
|
143
|
-
- **hex 编解码一度是端到端的大头**:SM4 4 KB ECB 的耗时里 hex **输出**曾占 69%
|
|
144
|
-
(`bytesToHex` 逐字节 `toString(16).padStart` 拼字符串)。改走 Node 的
|
|
145
|
-
`Buffer.toString('hex')`(浏览器回落查表)后编码快约 27×(4 KB:52.6 → 1.9 µs);
|
|
146
|
-
输入侧把「正则校验 + 解析」两趟合并成一趟——`Buffer.from(s, 'hex')` 遇到非法字符会
|
|
147
|
-
提前停止,比对「解析出的字节数 × 2 == 输入长度」即完成校验(8192 字符 14.1 → 3.5 µs)。
|
|
148
|
-
两者合计让 **hex 入参的 SM3 / SM4 / ZUC 端到端快 27~32%**(SM4 4 KB:35.8 → 25.1 µs),
|
|
149
|
-
`Uint8Array` 入参不受影响;SM2 / SM9 本体在毫秒级,这部分在其总耗时里体现不出来;
|
|
150
|
-
- 数字随版本与机器变化,请以本机 `pnpm bench` 为准。
|
|
41
|
+
热点(大整数模运算、hash 压缩函数、分组密码轮函数、双线性对)在 WASM 内执行。
|
|
42
|
+
所有经过对比的公开操作都快于同机纯 JS 参考实现:SM3 / ZUC 快 1.8\~2.3×,
|
|
43
|
+
SM4 六种模式 3.6\~11.2×,SM2 1.8\~7.6×。
|
|
44
|
+
|
|
45
|
+
完整的逐项操作表、与 `gmkitx` 的对比,以及「为什么是这个形状」的分析,见
|
|
46
|
+
**[性能页](https://jktantan.github.io/sm-crypto-wasm/performance)**。本机复现:
|
|
47
|
+
`pnpm bench`。
|
|
151
48
|
|
|
152
49
|
## 安装
|
|
153
50
|
|
|
@@ -307,14 +204,51 @@ import { encryptCBC } from 'sm-crypto-wasm/sm4';
|
|
|
307
204
|
|
|
308
205
|
## 跨运行时
|
|
309
206
|
|
|
310
|
-
| 环境 |
|
|
207
|
+
| 环境 | 加载路径 |
|
|
311
208
|
|---|---|
|
|
312
|
-
| Node 18+ | `fs/promises` 读取 `dist/<algo>/<algo>.wasm` |
|
|
313
|
-
|
|
|
209
|
+
| Node 18+ | `fs/promises` 读取 `dist/<algo>/<algo>.wasm`,可用 `loadWasmSync()` 同步预热 |
|
|
210
|
+
| 浏览器(经打包器) | `fetch(new URL('../<algo>/<algo>.wasm', import.meta.url))`,由打包器归档并重写 URL |
|
|
314
211
|
| Web Worker | 加载器不依赖 DOM,Worker 内可用 |
|
|
315
212
|
| Deno / Bun | 走 Node 同路径 |
|
|
316
213
|
|
|
317
|
-
|
|
214
|
+
### 打包器支持
|
|
215
|
+
|
|
216
|
+
包内按环境提供两份实现(Node 版读文件 / 浏览器版 `fetch`),由 `package.json` 的
|
|
217
|
+
`browser` 字段在打包时自动切换。因此**浏览器产物里不会出现 `node:*`** —— 不做 node 内置
|
|
218
|
+
模块外部化的打包器(Webpack、esbuild、Rspack)也不会在解析期报 `Can't resolve 'node:fs'`。
|
|
219
|
+
|
|
220
|
+
| 打包器 | 需要配置吗 | wasm 资源 |
|
|
221
|
+
|---|---|---|
|
|
222
|
+
| **Vite 5** | 零配置 | 自动归档进产物(`assets/*.wasm`) |
|
|
223
|
+
| **Webpack 5** | 零配置 | 作为 asset 自动产出 |
|
|
224
|
+
| **Rspack 2** | 零配置 | 作为 asset 自动产出(带 hash 文件名) |
|
|
225
|
+
| **Turbopack**(Next.js 16 默认) | 零配置 | 归档到 `static/media/*.wasm` |
|
|
226
|
+
| **esbuild** | 加一行插件(见下) | esbuild 不处理 `new URL(..., import.meta.url)` 指向的文件 |
|
|
227
|
+
| **Node 18+** | 零配置 | 读 `dist/<algo>/<algo>.wasm` |
|
|
228
|
+
|
|
229
|
+
esbuild 的用户装一个自带的小插件,它会自动把 wasm 放到运行时能解析到的位置:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
import { smCryptoWasm } from 'sm-crypto-wasm/esbuild-plugin';
|
|
233
|
+
|
|
234
|
+
await esbuild.build({
|
|
235
|
+
entryPoints: ['src/main.ts'],
|
|
236
|
+
bundle: true,
|
|
237
|
+
platform: 'browser',
|
|
238
|
+
format: 'esm',
|
|
239
|
+
outdir: 'dist/assets', // 产物 JS 落这里
|
|
240
|
+
plugins: [smCryptoWasm()], // → wasm 复制到 dist/<algo>/<algo>.wasm
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
不想用插件也可以:把 `node_modules/sm-crypto-wasm/dist/{sm2,sm3,sm4,sm9,zuc}`
|
|
245
|
+
复制到**产物 JS 的上级目录**(上例即 `dist/`)。
|
|
246
|
+
|
|
247
|
+
浏览器需要**安全上下文**(`https://` 或 `localhost`)才能拿到 `crypto.getRandomValues`;
|
|
248
|
+
拿不到时库会**抛错**(fail-closed),不会退化成非密码学随机源。
|
|
249
|
+
|
|
250
|
+
Cloudflare Workers / Vercel Edge 这类"严格流式 WASM"环境**不在当前范围**;
|
|
251
|
+
`<script type="module">` 直载(不经打包器)同样不支持。
|
|
318
252
|
|
|
319
253
|
---
|
|
320
254
|
|
|
@@ -423,153 +357,14 @@ lazy-loaded on demand.
|
|
|
423
357
|
|
|
424
358
|
## Performance
|
|
425
359
|
|
|
426
|
-
Hot paths (bignum modular arithmetic, hash compression, block-cipher rounds
|
|
427
|
-
inside WASM.
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|---|---|---|---|---|
|
|
435
|
-
| SM3 | hash 32 B / 1 KB / 64 KB | 1.04 µs / 6.27 µs / 330 µs | 13.7 µs (1 KB) | 2.2× faster |
|
|
436
|
-
| SM3 | hmac 1 KB | 10.1 µs | 17.9 µs | 1.8× faster |
|
|
437
|
-
| SM4 | ECB 16 B / 1 KB | 0.94 µs / 6.98 µs | 37.3 µs (1 KB) | 5.3× faster |
|
|
438
|
-
| SM4 | CBC 1 KB | 10.5 µs | 41.6 µs | 4.0× faster |
|
|
439
|
-
| SM4 | CTR 1 KB | 7.43 µs | 36.5 µs | 4.9× faster |
|
|
440
|
-
| SM4 | CFB 1 KB | 10.0 µs | 37.9 µs | 3.8× faster |
|
|
441
|
-
| SM4 | OFB 1 KB | 9.98 µs | 36.3 µs | 3.6× faster |
|
|
442
|
-
| SM4 | GCM 1 KB | 19.8 µs | 221 µs | 11.2× faster |
|
|
443
|
-
| SM2 | generateKeyPair | 151 µs | — | — |
|
|
444
|
-
| SM2 | public key derive | 110 µs | 197 µs | 1.8× faster |
|
|
445
|
-
| SM2 | sign 64 B | 142 µs | 446 µs | 3.1× faster |
|
|
446
|
-
| SM2 | verify 64 B | 402 µs | 2.85 ms | 7.1× faster |
|
|
447
|
-
| SM2 | encrypt 64 B | 499 µs | 2.96 ms | 5.9× faster |
|
|
448
|
-
| SM2 | decrypt 64 B | 343 µs | 2.59 ms | 7.6× faster |
|
|
449
|
-
| SM2 | key exchange (full A↔B) | 1.52 ms | 11.4 ms | 7.5× faster |
|
|
450
|
-
| SM9 | master key pair (sign / enc) | 2.07 / 1.73 ms | — | — |
|
|
451
|
-
| SM9 | master public key (sign / enc) | 97.1 / 49.1 µs | — | — |
|
|
452
|
-
| SM9 | user public key (derive) | 563 µs | — | — |
|
|
453
|
-
| SM9 | sign 32 B | 4.11 ms | — | — |
|
|
454
|
-
| SM9 | verify 32 B | 8.00 ms | — | — |
|
|
455
|
-
| SM9 | encrypt 32 B | 3.69 ms | — | — |
|
|
456
|
-
| SM9 | decrypt 32 B | 5.40 ms | — | — |
|
|
457
|
-
| SM9 | KEM encapsulate / decapsulate | 3.42 / 4.92 ms | — | — |
|
|
458
|
-
| SM9 | key exchange (full A↔B) | 19.6 ms | — | — |
|
|
459
|
-
| ZUC | EEA3 encrypt 1 KB | 9.07 µs | 20.5 µs | 2.3× faster |
|
|
460
|
-
| ZUC | EEA3 keystream 1 KB | 6.15 µs | — | — |
|
|
461
|
-
| ZUC | EIA3 MAC 1 KB | 11.2 µs | — | — |
|
|
462
|
-
|
|
463
|
-
> **On the SM2 rows**: this layer is the TS facade API (signing goes through the
|
|
464
|
-
> public-key derivation cache; encryption still does the hex and curve-point codecs
|
|
465
|
-
> and calls the host CSPRNG on every call), and it fluctuates up to ±20% between
|
|
466
|
-
> runs — far more than SM3 / SM4 / SM9 / ZUC. The SM2 numbers in the table come from
|
|
467
|
-
> a single run (the `wasm` and `gmkitx` columns are same-source); the **net gain at
|
|
468
|
-
> the wasm compute layer** (more stable, min-of-5 sampled) is in the Notes below.
|
|
469
|
-
|
|
470
|
-
Notes:
|
|
471
|
-
|
|
472
|
-
- **Every operation with a counterpart beats the pure-JS reference**: SM3 / ZUC by
|
|
473
|
-
1.8–2.3×, the six SM4 modes by 3.6–11.2×, SM2 by 1.8–7.6×;
|
|
474
|
-
- **SM4's CFB / OFB used to be 1.4–1.5× *slower* than pure JS**: those two modes
|
|
475
|
-
were driven block-by-block from the TS side (alloc + call + copy back every 16
|
|
476
|
-
bytes — 64 wasm crossings per KB). With the same batched export CBC/CTR already
|
|
477
|
-
had, they now land at ~10 µs, i.e. **3.6–3.8× faster** than the reference;
|
|
478
|
-
- **SM4's bottleneck is the four S-box lookups per round**: merging the S-box and L
|
|
479
|
-
transform into T-tables bought only 8–10% (the lookup count is unchanged), because
|
|
480
|
-
the cost is memory access, not ALU work. Since the lookup count cannot be reduced,
|
|
481
|
-
the library hides it behind instruction-level parallelism instead: ECB's multi-block
|
|
482
|
-
loop is **8 blocks interleaved in scalar code** (each block keeps four independent
|
|
483
|
-
dependency chains, and out-of-order execution covers the L1 latency), CTR / GCM
|
|
484
|
-
keystream generation uses the same width, and shorter remainders fall back to 4-way
|
|
485
|
-
then single-block. Why not WASM SIMD: `v128` has neither AVX2's gather (so 8→32-bit
|
|
486
|
-
tables cannot be looked up in parallel) nor AES-NI, and `i8x16.swizzle` only reaches
|
|
487
|
-
4-bit S-boxes, not SM4's 8-bit one — the lookup count stays the same, leaving only
|
|
488
|
-
lane parallelism for the XOR chain, which in practice measured slower than the
|
|
489
|
-
scalar interleave. This also explains the remaining gap to C: OpenSSL uses ARMv8's
|
|
490
|
-
`SM4E` instruction to implement the S-box in circuitry with **no table lookup at
|
|
491
|
-
all**, and WebAssembly does not expose that instruction set (generic SIMD only
|
|
492
|
-
offers generic operators);
|
|
493
|
-
- **SM9 has no comparable implementation** (as usual for pure JS, where pairing is
|
|
494
|
-
prohibitively expensive), so only this library's absolute timings are given;
|
|
495
|
-
- **SM2's wasm big integers are linear-memory raw pointers** (`U256` is a `usize`
|
|
496
|
-
backed by a module-level bump arena that rewinds at every exported entry), and
|
|
497
|
-
on top of that eight things changed:
|
|
498
|
-
1. **Reduction in the prime field p uses Solinas** — p = 2^256−2^224−2^96+2^64−1
|
|
499
|
-
is pseudo-Mersenne, so expanding 2^256 ≡ 2^224+2^96−2^64+1 makes reduction
|
|
500
|
-
pure add/subtract/shift with no multiplication at all (one reduction measures
|
|
501
|
-
0.03 µs, an order of magnitude below generic long division);
|
|
502
|
-
2. **Squaring was rewritten as Comba (column scan) with a single carry pass** —
|
|
503
|
-
the old version ran two full carry-propagation loops per (i,j) pair (56 memory
|
|
504
|
-
loops in total), making it the single most expensive function in the library;
|
|
505
|
-
3. **Scalar multiplication takes two routes**: fixed-base uses a comb table
|
|
506
|
-
(w=5, 32 entries; 51 doublings + 52 additions), arbitrary-point uses wNAF
|
|
507
|
-
(w=5, cutting additions from ~128 to ~43). The table is built lazily once and
|
|
508
|
-
cached in a permanent area outside the arena;
|
|
509
|
-
4. **Point arithmetic is fully in-place**: doubling and addition write
|
|
510
|
-
module-level scratch instead of allocating a fresh `U256` per operation plus
|
|
511
|
-
a `JPoint` (a single doubling used to cost ~11 heap allocations, and scalar
|
|
512
|
-
multiplication calls it hundreds of times);
|
|
513
|
-
5. **Point addition uses mixed coordinates** — the comb table and G's wNAF table
|
|
514
|
-
are converted to affine at build time with one **Montgomery batch inversion**
|
|
515
|
-
(n inversions → one inversion + 3n multiplications; the 32-entry table costs
|
|
516
|
-
~25 µs, once), which drops the per-addition cost inside the loop from 12M+4S
|
|
517
|
-
to **8M+3S** (about a third off);
|
|
518
|
-
6. **Verification uses a joint scalar multiplication** — `s·G + t·P` runs two
|
|
519
|
-
wNAFs that share one chain of doublings (G's 16-entry precomputed table is
|
|
520
|
-
cached affine as well and uses mixed coordinates), saving the 51 doublings
|
|
521
|
-
the comb route would spend;
|
|
522
|
-
7. **Inversion uses a local word-level binary extended Euclid** — `u`, `v` and
|
|
523
|
-
both Bézout coefficients live in wasm locals (256 bits fit exactly 4 u64s),
|
|
524
|
-
so each round makes zero memory round-trips; one inversion went 12.8 → 2.3 µs
|
|
525
|
-
(SM2 and SM9 share this path, so SM9 benefits too);
|
|
526
|
-
8. **`sign` derives its public key through an in-process cache** — the public key
|
|
527
|
-
and the Z value depend only on the private key, yet every signing used to redo
|
|
528
|
-
the fixed-base scalar multiplication (measured 105 µs, 41% of a ~250 µs
|
|
529
|
-
signing); the cache is keyed by the private key's hex, bounded at 32 entries
|
|
530
|
-
with FIFO eviction, and a hit skips the derivation. `sign` went 252 → 142 µs.
|
|
531
|
-
Items 1–7 are at the wasm compute layer, item 8 is in the TS facade; their A/B
|
|
532
|
-
tests (min-of-5, same machine, same probe) give scalar multiplication of G
|
|
533
|
-
138 → 105 µs, inversion 12.8 → 2.3 µs, signing 156 → 122 µs, verification
|
|
534
|
-
474 → 424 µs, encryption 487 → 449 µs, key exchange 563 → 511 µs; the earlier
|
|
535
|
-
accumulation (Solinas + Comba + comb/wNAF + in-placing) is what brought signing
|
|
536
|
-
down from 6.10 ms;
|
|
537
|
-
**trade-off**: the public `getPublicKey` still always computes (keeping the
|
|
538
|
-
"derive a public key" semantics and its benchmark measurable), while the cache
|
|
539
|
-
leaves the private key's derived result (and the key's own hex form) resident in
|
|
540
|
-
process memory — call `configurePublicKeyCache({ enabled: false })` to turn it
|
|
541
|
-
off and go back to recomputing every time. `encrypt` only takes a public key, so
|
|
542
|
-
no derivation is involved there;
|
|
543
|
-
- **Trade-offs**: comb applies only to the fixed base point and Solinas only to
|
|
544
|
-
pseudo-Mersenne moduli — SM2's scalar modulus n and SM9's p/n are not of that
|
|
545
|
-
shape, so SM9's base field now runs in the Montgomery domain (next bullet);
|
|
546
|
-
**mixed coordinates** require affine table entries, so
|
|
547
|
-
every precomputed table pays one batch inversion up front — G's two tables are
|
|
548
|
-
cached in the permanent area and amortized, while an arbitrary point's wNAF table
|
|
549
|
-
is rebuilt each time, where the batch inversion roughly cancels the additions it
|
|
550
|
-
saves, so it stays Jacobian; **joint scalar multiplication** only applies to the
|
|
551
|
-
"one fixed base + one arbitrary point" shape of `s·G + t·P`, not to a plain
|
|
552
|
-
single-point scalar multiplication;
|
|
553
|
-
- SM9 is dominated by pairings. Its base field Fp now runs in the **Montgomery
|
|
554
|
-
domain** (CIOS, R = 2²⁵⁶): SM9's p is a BN parameter, not pseudo-Mersenne, so one
|
|
555
|
-
modular multiply used to pay Barrett's 64 partial products plus ~153 reduction
|
|
556
|
-
multiplies, where CIOS needs only 2×64. Elements are a·R throughout the field,
|
|
557
|
-
converted once at the wasm boundary (bytes ↔ elements, module constants). A
|
|
558
|
-
single modular multiply went 179 → 98 ns (1.8×), which carries through end to
|
|
559
|
-
end as **1.6–1.8×** on sign / verify / encrypt / decrypt / KEM / key exchange;
|
|
560
|
-
laying the extension-field elements (Fp2/Fp4/Fp12) out as contiguous memory is
|
|
561
|
-
what made sign / verify ~2.6× faster than earlier versions;
|
|
562
|
-
- **hex codecs used to be a large part of the end-to-end cost**: hex *output* once
|
|
563
|
-
accounted for 69% of SM4's 4 KB ECB time (`bytesToHex` built the string byte by
|
|
564
|
-
byte with `toString(16).padStart`). Routing to Node's `Buffer.toString('hex')`
|
|
565
|
-
(browser falls back to a lookup table) made encoding ~27× faster (4 KB: 52.6 →
|
|
566
|
-
1.9 µs); on the input side, "regex validation + parse" collapsed into a single
|
|
567
|
-
pass — `Buffer.from(s, 'hex')` stops at the first invalid character, so comparing
|
|
568
|
-
"parsed bytes × 2 == input length" *is* the validation (8192 chars: 14.1 →
|
|
569
|
-
3.5 µs). Together they make **SM3 / SM4 / ZUC with hex input 27–32% faster
|
|
570
|
-
end-to-end** (SM4 4 KB: 35.8 → 25.1 µs), while `Uint8Array` input is unaffected;
|
|
571
|
-
SM2 / SM9 take milliseconds, so this is invisible in their totals;
|
|
572
|
-
- Numbers vary with version and machine — run `pnpm bench` locally.
|
|
360
|
+
Hot paths (bignum modular arithmetic, hash compression, block-cipher rounds,
|
|
361
|
+
pairings) run inside WASM. **Every operation with a pure-JS counterpart is faster
|
|
362
|
+
than it**: SM3 / ZUC by 1.8–2.3×, the six SM4 modes by 3.6–11.2×, SM2 by 1.8–7.6×.
|
|
363
|
+
|
|
364
|
+
The full per-operation table, the head-to-head against `gmkitx`, and the
|
|
365
|
+
"why it looks like this" notes live on the
|
|
366
|
+
**[performance page](https://jktantan.github.io/sm-crypto-wasm/en/performance)**.
|
|
367
|
+
Reproduce locally with `pnpm bench`.
|
|
573
368
|
|
|
574
369
|
## Installation
|
|
575
370
|
|
|
@@ -740,15 +535,55 @@ Available sub-paths: `sm-crypto-wasm/sm2` · `/sm3` · `/sm4` · `/sm9` · `/zuc
|
|
|
740
535
|
|
|
741
536
|
## Cross-runtime
|
|
742
537
|
|
|
743
|
-
| Environment |
|
|
538
|
+
| Environment | Loading path |
|
|
744
539
|
|---|---|
|
|
745
|
-
| Node 18+ | reads `dist/<algo>/<algo>.wasm` via `fs/promises` |
|
|
746
|
-
| Browser
|
|
540
|
+
| Node 18+ | reads `dist/<algo>/<algo>.wasm` via `fs/promises`; `loadWasmSync()` available |
|
|
541
|
+
| Browser (via a bundler) | `fetch(new URL('../<algo>/<algo>.wasm', import.meta.url))`; the bundler archives and rewrites it |
|
|
747
542
|
| Web Worker | the loader is DOM-free and works inside a Worker |
|
|
748
543
|
| Deno / Bun | follows the Node path |
|
|
749
544
|
|
|
750
|
-
|
|
751
|
-
|
|
545
|
+
### Bundler support
|
|
546
|
+
|
|
547
|
+
The package ships two implementations (a Node one reading files, a browser one
|
|
548
|
+
using `fetch`) and `package.json`'s `browser` field switches between them at
|
|
549
|
+
build time. So **`node:*` never appears in a browser bundle** — bundlers that do
|
|
550
|
+
not externalise node built-ins (Webpack, esbuild, Rspack) no longer fail at
|
|
551
|
+
resolve time with `Can't resolve 'node:fs'`.
|
|
552
|
+
|
|
553
|
+
| Bundler | Configuration | wasm asset |
|
|
554
|
+
|---|---|---|
|
|
555
|
+
| **Vite 5** | none | archived into the output (`assets/*.wasm`) |
|
|
556
|
+
| **Webpack 5** | none | emitted as an asset |
|
|
557
|
+
| **Rspack 2** | none | emitted as an asset (hashed filenames) |
|
|
558
|
+
| **Turbopack** (Next.js 16 default) | none | archived under `static/media/*.wasm` |
|
|
559
|
+
| **esbuild** | one plugin line (below) | esbuild does not handle files pointed at by `new URL(..., import.meta.url)` |
|
|
560
|
+
| **Node 18+** | none | reads `dist/<algo>/<algo>.wasm` |
|
|
561
|
+
|
|
562
|
+
esbuild users add a small bundled plugin that drops the wasm where the runtime looks for it:
|
|
563
|
+
|
|
564
|
+
```ts
|
|
565
|
+
import { smCryptoWasm } from 'sm-crypto-wasm/esbuild-plugin';
|
|
566
|
+
|
|
567
|
+
await esbuild.build({
|
|
568
|
+
entryPoints: ['src/main.ts'],
|
|
569
|
+
bundle: true,
|
|
570
|
+
platform: 'browser',
|
|
571
|
+
format: 'esm',
|
|
572
|
+
outdir: 'dist/assets', // output JS lands here
|
|
573
|
+
plugins: [smCryptoWasm()], // → wasm copied to dist/<algo>/<algo>.wasm
|
|
574
|
+
});
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
Prefer no plugin? Copy `node_modules/sm-crypto-wasm/dist/{sm2,sm3,sm4,sm9,zuc}`
|
|
578
|
+
into the **parent** directory of the output JS (i.e. `dist/` in the example above).
|
|
579
|
+
|
|
580
|
+
Browsers need a **secure context** (`https://` or `localhost`) for
|
|
581
|
+
`crypto.getRandomValues`; when it is missing the library **throws** (fail-closed)
|
|
582
|
+
rather than falling back to a non-cryptographic source.
|
|
583
|
+
|
|
584
|
+
Strict-streaming WASM environments such as Cloudflare Workers / Vercel Edge are
|
|
585
|
+
**out of scope**; so is loading the raw ESM output directly via
|
|
586
|
+
`<script type="module">` (without a bundler).
|
|
752
587
|
|
|
753
588
|
---
|
|
754
589
|
|
|
@@ -883,153 +718,13 @@ ZUC**) на **AssemblyScript → WebAssembly**. Работает на Node 18+
|
|
|
883
718
|
## Производительность
|
|
884
719
|
|
|
885
720
|
Горячие пути (модульная арифметика больших чисел, сжатие хэша, раунды блочного
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|---|---|---|---|---|
|
|
894
|
-
| SM3 | hash 32 B / 1 KB / 64 KB | 1.04 µs / 6.27 µs / 330 µs | 13.7 µs (1 KB) | в 2.2× быстрее |
|
|
895
|
-
| SM3 | hmac 1 KB | 10.1 µs | 17.9 µs | в 1.8× быстрее |
|
|
896
|
-
| SM4 | ECB 16 B / 1 KB | 0.94 µs / 6.98 µs | 37.3 µs (1 KB) | в 5.3× быстрее |
|
|
897
|
-
| SM4 | CBC 1 KB | 10.5 µs | 41.6 µs | в 4.0× быстрее |
|
|
898
|
-
| SM4 | CTR 1 KB | 7.43 µs | 36.5 µs | в 4.9× быстрее |
|
|
899
|
-
| SM4 | CFB 1 KB | 10.0 µs | 37.9 µs | в 3.8× быстрее |
|
|
900
|
-
| SM4 | OFB 1 KB | 9.98 µs | 36.3 µs | в 3.6× быстрее |
|
|
901
|
-
| SM4 | GCM 1 KB | 19.8 µs | 221 µs | в 11.2× быстрее |
|
|
902
|
-
| SM2 | generateKeyPair | 151 µs | — | — |
|
|
903
|
-
| SM2 | вывод открытого ключа | 110 µs | 197 µs | в 1.8× быстрее |
|
|
904
|
-
| SM2 | подпись 64 B | 142 µs | 446 µs | в 3.1× быстрее |
|
|
905
|
-
| SM2 | проверка 64 B | 402 µs | 2.85 ms | в 7.1× быстрее |
|
|
906
|
-
| SM2 | шифрование 64 B | 499 µs | 2.96 ms | в 5.9× быстрее |
|
|
907
|
-
| SM2 | расшифрование 64 B | 343 µs | 2.59 ms | в 7.6× быстрее |
|
|
908
|
-
| SM2 | обмен ключами (полный A↔B) | 1.52 ms | 11.4 ms | в 7.5× быстрее |
|
|
909
|
-
| SM9 | главная пара ключей (sign / enc) | 2.07 / 1.73 ms | — | — |
|
|
910
|
-
| SM9 | главный открытый ключ (sign / enc) | 97.1 / 49.1 µs | — | — |
|
|
911
|
-
| SM9 | открытый ключ пользователя | 563 µs | — | — |
|
|
912
|
-
| SM9 | подпись 32 B | 4.11 ms | — | — |
|
|
913
|
-
| SM9 | проверка 32 B | 8.00 ms | — | — |
|
|
914
|
-
| SM9 | шифрование 32 B | 3.69 ms | — | — |
|
|
915
|
-
| SM9 | расшифрование 32 B | 5.40 ms | — | — |
|
|
916
|
-
| SM9 | KEM инкапсуляция / декапсуляция | 3.42 / 4.92 ms | — | — |
|
|
917
|
-
| SM9 | обмен ключами (полный A↔B) | 19.6 ms | — | — |
|
|
918
|
-
| ZUC | EEA3 шифрование 1 KB | 9.07 µs | 20.5 µs | в 2.3× быстрее |
|
|
919
|
-
| ZUC | EEA3 гамма 1 KB | 6.15 µs | — | — |
|
|
920
|
-
| ZUC | EIA3 MAC 1 KB | 11.2 µs | — | — |
|
|
921
|
-
|
|
922
|
-
> **О строках SM2**: этот слой — фасадный API на TS (подпись идёт через кэш вывода
|
|
923
|
-
> открытого ключа, а шифрование по-прежнему выполняет кодек hex и точек кривой и
|
|
924
|
-
> вызывает CSPRNG хоста на каждом вызове), и между запусками он колеблется до ±20% —
|
|
925
|
-
> намного сильнее, чем SM3 / SM4 / SM9 / ZUC. Числа SM2 в таблице взяты из одного
|
|
926
|
-
> запуска (столбцы `wasm` и `gmkitx` из одного источника); **чистый выигрыш на уровне
|
|
927
|
-
> вычислений wasm** (стабильнее, выборка min-of-5) приведён в примечаниях ниже.
|
|
928
|
-
|
|
929
|
-
Примечания:
|
|
930
|
-
|
|
931
|
-
- **Каждая операция, у которой есть аналог, быстрее чистой JS-реализации**:
|
|
932
|
-
SM3 / ZUC — в 1.8–2.3×, шесть режимов SM4 — в 3.6–11.2×, SM2 — в 1.8–7.6×;
|
|
933
|
-
- **CFB / OFB у SM4 раньше были в 1.4–1.5× _медленнее_ чистой JS**: эти два режима
|
|
934
|
-
вызывались поблочно со стороны TS (alloc + вызов + копирование на каждые 16 байт —
|
|
935
|
-
64 пересечения границы на 1 KB). С тем же пакетным экспортом, что уже был у CBC/CTR,
|
|
936
|
-
они теперь ~10 µs, то есть **в 3.6–3.8× быстрее** аналога;
|
|
937
|
-
- **Узкое место SM4 — четыре обращения к S-блоку за раунд**: объединение S-блока и
|
|
938
|
-
L-преобразования в T-таблицы дало лишь 8–10% (число обращений не меняется), потому что
|
|
939
|
-
стоимость — это доступ к памяти, а не работа ALU. Раз число обращений снизить нельзя,
|
|
940
|
-
библиотека прячет их за параллелизмом на уровне инструкций: цикл по блокам в ECB сделан
|
|
941
|
-
как **8 блоков с чередованием в скалярном коде** (у каждого блока четыре независимые
|
|
942
|
-
цепочки зависимостей, а внеочередное исполнение скрывает задержку L1), генерация гаммы в
|
|
943
|
-
CTR / GCM использует ту же ширину, а короткие остатки переходят на 4 блока, затем на один.
|
|
944
|
-
Почему не WASM SIMD: у `v128` нет ни gather из AVX2 (нельзя параллельно индексировать
|
|
945
|
-
таблицы 8→32 бита), ни AES-NI, а `i8x16.swizzle` дотягивается лишь до 4-битных S-блоков,
|
|
946
|
-
но не до 8-битного S-блока SM4 — число обращений не меняется, остаётся только параллелизм
|
|
947
|
-
полос для цепочки XOR, и на практике это оказалось медленнее скалярного чередования. Это же
|
|
948
|
-
объясняет оставшийся разрыв с C: OpenSSL использует инструкцию ARMv8 `SM4E`, реализующую
|
|
949
|
-
S-блок схемой **вообще без обращений к таблицам**, а WebAssembly этого набора инструкций
|
|
950
|
-
не предоставляет (общий SIMD даёт только общие операторы);
|
|
951
|
-
- **У SM9 нет сопоставимой реализации** (обычная ситуация для чистого JS, где
|
|
952
|
-
спаривания запредельно дороги), поэтому приведены только абсолютные времена
|
|
953
|
-
этой библиотеки;
|
|
954
|
-
- **Большие числа в wasm-части SM2 — сырые указатели в линейной памяти** (`U256` —
|
|
955
|
-
это `usize` поверх bump-арены уровня модуля, которая откатывается на каждом входе
|
|
956
|
-
в экспортируемую функцию); поверх этого сделано восемь вещей:
|
|
957
|
-
1. **Приведение в простом поле p по схеме Солинаса** — p = 2^256−2^224−2^96+2^64−1
|
|
958
|
-
псевдо-мерсенново, поэтому после разложения 2^256 ≡ 2^224+2^96−2^64+1 приведение
|
|
959
|
-
состоит только из сложений, вычитаний и сдвигов, без умножений вообще (одно
|
|
960
|
-
приведение — 0.03 µs, на порядок меньше общего длинного деления);
|
|
961
|
-
2. **Возведение в квадрат переписано по схеме Comba (сканирование по столбцам) с
|
|
962
|
-
одним проходом переносов** — старая версия выполняла два полных цикла
|
|
963
|
-
распространения переносов на каждую пару (i,j) (56 циклов по памяти), из-за чего
|
|
964
|
-
это была самая дорогая функция в библиотеке;
|
|
965
|
-
3. **Скалярное умножение идёт двумя путями**: для фиксированной базовой точки —
|
|
966
|
-
comb-таблица (w=5, 32 записи; 51 удвоение + 52 сложения), для произвольной точки —
|
|
967
|
-
wNAF (w=5, сложений ~128 → ~43). Таблица строится лениво один раз и кэшируется в
|
|
968
|
-
постоянной области вне арены;
|
|
969
|
-
4. **Операции над точками полностью на месте**: удвоение и сложение пишут в
|
|
970
|
-
модульные scratch-буферы вместо `new U256` на каждую операцию плюс `JPoint`
|
|
971
|
-
(одно удвоение стоило ~11 выделений в куче, а скалярное умножение вызывает его
|
|
972
|
-
сотни раз);
|
|
973
|
-
5. **Сложение точек использует смешанные координаты** — comb-таблица и wNAF-таблица
|
|
974
|
-
для G переводятся в аффинные один раз при построении **пакетным обращением
|
|
975
|
-
Монтгомери** (n обращений → одно обращение + 3n умножений; таблица на 32 записи
|
|
976
|
-
стоит ~25 µs и только один раз), поэтому стоимость одного сложения в цикле падает
|
|
977
|
-
с 12M+4S до **8M+3S** (примерно на треть);
|
|
978
|
-
6. **Проверка подписи использует совместное скалярное умножение** — `s·G + t·P`
|
|
979
|
-
выполняется двумя wNAF, разделяющими одну цепочку удвоений (предвычисленная
|
|
980
|
-
таблица G на 16 записей тоже кэшируется аффинной и использует смешанные
|
|
981
|
-
координаты), что экономит 51 удвоение пути comb;
|
|
982
|
-
7. **Обращение переписано на локальный пословный двоичный расширенный алгоритм
|
|
983
|
-
Евклида** — `u`, `v` и оба коэффициента Безу живут в локальных переменных wasm
|
|
984
|
-
(256 бит — ровно 4 u64), поэтому каждый раунд обходится без обращений к памяти;
|
|
985
|
-
одно обращение 12.8 → 2.3 µs (SM2 и SM9 используют этот путь совместно, поэтому
|
|
986
|
-
SM9 тоже выигрывает);
|
|
987
|
-
8. **`sign` выводит открытый ключ через внутрипроцессный кэш** — открытый ключ и
|
|
988
|
-
значение Z зависят только от закрытого ключа, но каждая подпись заново выполняла
|
|
989
|
-
умножение на скаляр с фиксированной базой (измерено 105 µs — 41% от ~250 µs
|
|
990
|
-
подписи); кэш использует в качестве ключа hex закрытого ключа, ограничен 32
|
|
991
|
-
записями с вытеснением FIFO, а попадание пропускает вывод. `sign` 252 → 142 µs.
|
|
992
|
-
Пункты 1–7 — на уровне вычислений wasm, пункт 8 — в фасаде TS; их A/B (min-of-5,
|
|
993
|
-
та же машина, тот же probe): скалярное умножение G 138 → 105 µs, обращение
|
|
994
|
-
12.8 → 2.3 µs, подпись 156 → 122 µs, проверка 474 → 424 µs, шифрование 487 → 449 µs,
|
|
995
|
-
обмен ключами 563 → 511 µs; накопленное ранее (Солинас + Comba + comb/wNAF +
|
|
996
|
-
на месте) и снизило подпись с 6.10 ms;
|
|
997
|
-
**компромисс**: публичный `getPublicKey` по-прежнему считает каждый раз (сохраняя
|
|
998
|
-
семантику «вывести открытый ключ» и измеримость бенчмарка), а кэш оставляет
|
|
999
|
-
производный результат закрытого ключа (и его hex-форму) в памяти процесса — отключить
|
|
1000
|
-
можно через `configurePublicKeyCache({ enabled: false })`, вернувшись к пересчёту
|
|
1001
|
-
каждый раз. `encrypt` принимает только открытый ключ, вывода там нет;
|
|
1002
|
-
- **Компромиссы**: comb применим только к фиксированной базовой точке, Солинас — только
|
|
1003
|
-
к псевдо-мерсенновым модулям: скалярный модуль n у SM2 и p/n у SM9 не такой формы,
|
|
1004
|
-
поэтому базовое поле SM9 теперь работает в области Монтгомери (следующий пункт);
|
|
1005
|
-
**смешанные координаты** требуют аффинных записей таблиц, поэтому каждая
|
|
1006
|
-
предвычисленная таблица сначала платит одно пакетное обращение — обе таблицы G
|
|
1007
|
-
кэшируются в постоянной области и амортизируются, а wNAF-таблица произвольной точки
|
|
1008
|
-
строится каждый раз, где пакетное обращение примерно компенсирует сэкономленные
|
|
1009
|
-
сложения, поэтому там остаётся якобиан; **совместное скалярное умножение** работает
|
|
1010
|
-
только для формы «одна фиксированная база + одна произвольная точка» (`s·G + t·P`),
|
|
1011
|
-
но не для обычного умножения на скаляр одной точки;
|
|
1012
|
-
- SM9 определяется спариваниями. Его базовое поле Fp теперь работает в
|
|
1013
|
-
**области Монтгомери** (CIOS, R = 2²⁵⁶): p у SM9 — параметр BN, а не
|
|
1014
|
-
псевдо-мерсенново число, поэтому одно модульное умножение платило 64 частичных
|
|
1015
|
-
произведения Барретта плюс ~153 умножения на этапе приведения, тогда как CIOS
|
|
1016
|
-
требует только 2×64. Элементы внутри поля всюду имеют вид a·R и преобразуются
|
|
1017
|
-
один раз на границе wasm (байты ↔ элементы, константы модуля). Одно модульное
|
|
1018
|
-
умножение ускорилось с 179 до 98 нс (1.8×), что переносится в сквозной результат
|
|
1019
|
-
**1.6–1.8×** на sign / verify / encrypt / decrypt / KEM / обмене ключами; а переход
|
|
1020
|
-
элементов расширения (Fp2/Fp4/Fp12) на непрерывную память — это то, что дало
|
|
1021
|
-
ускорение sign / verify примерно в 2.6× относительно более ранних версий;
|
|
1022
|
-
- **Кодеки hex раньше были большой частью сквозной стоимости**: вывод hex когда-то
|
|
1023
|
-
занимал 69% времени SM4 4 KB ECB (`bytesToHex` собирал строку побайтово через
|
|
1024
|
-
`toString(16).padStart`). После перехода на `Buffer.toString('hex')` в Node (в
|
|
1025
|
-
браузере — таблица поиска) кодирование стало примерно в 27× быстрее (4 KB: 52.6 →
|
|
1026
|
-
1.9 µs); на входе «проверка регулярным выражением + разбор» слились в один проход —
|
|
1027
|
-
`Buffer.from(s, 'hex')` останавливается на первом недопустимом символе, поэтому
|
|
1028
|
-
сравнение «разобранных байт × 2 == длина входа» *и есть* проверка (8192 символа:
|
|
1029
|
-
14.1 → 3.5 µs). Вместе это делает **SM3 / SM4 / ZUC с hex-входом на 27–32% быстрее
|
|
1030
|
-
сквозным образом** (SM4 4 KB: 35.8 → 25.1 µs), а вход `Uint8Array` не затрагивается;
|
|
1031
|
-
SM2 / SM9 измеряются миллисекундами, поэтому там это не видно;
|
|
1032
|
-
- Цифры зависят от версии и машины — запускайте `pnpm bench` локально.
|
|
721
|
+
шифра, спаривания) выполняются внутри WASM. **Каждая операция, у которой есть
|
|
722
|
+
чистый JS-аналог, быстрее его**: SM3 / ZUC — в 1.8–2.3×, шесть режимов SM4 —
|
|
723
|
+
в 3.6–11.2×, SM2 — в 1.8–7.6×.
|
|
724
|
+
|
|
725
|
+
Полная таблица по операциям, сравнение с `gmkitx` и разбор «почему именно так» —
|
|
726
|
+
на **[странице производительности](https://jktantan.github.io/sm-crypto-wasm/ru/performance)**.
|
|
727
|
+
Локально воспроизводится через `pnpm bench`.
|
|
1033
728
|
|
|
1034
729
|
## Установка
|
|
1035
730
|
|
|
@@ -1200,15 +895,56 @@ import { encryptCBC } from 'sm-crypto-wasm/sm4';
|
|
|
1200
895
|
|
|
1201
896
|
## Кросс-рантайм
|
|
1202
897
|
|
|
1203
|
-
| Среда |
|
|
898
|
+
| Среда | Путь загрузки |
|
|
1204
899
|
|---|---|
|
|
1205
|
-
| Node 18+ | читает `dist/<algo>/<algo>.wasm` через `fs/promises` |
|
|
1206
|
-
| Браузер
|
|
900
|
+
| Node 18+ | читает `dist/<algo>/<algo>.wasm` через `fs/promises`; доступен `loadWasmSync()` |
|
|
901
|
+
| Браузер (через сборщик) | `fetch(new URL('../<algo>/<algo>.wasm', import.meta.url))`; сборщик кладёт файл в вывод и переписывает URL |
|
|
1207
902
|
| Web Worker | загрузчик не зависит от DOM и работает внутри Worker'а |
|
|
1208
903
|
| Deno / Bun | идут по Node-пути |
|
|
1209
904
|
|
|
1210
|
-
|
|
1211
|
-
|
|
905
|
+
### Поддержка сборщиков
|
|
906
|
+
|
|
907
|
+
Пакет поставляет две реализации (Node-версия читает файлы, браузерная использует
|
|
908
|
+
`fetch`), а поле `browser` в `package.json` переключает их на этапе сборки. Поэтому
|
|
909
|
+
**`node:*` не попадает в браузерный бандл** — сборщики, которые не выносят встроенные
|
|
910
|
+
модули Node наружу (Webpack, esbuild, Rspack), больше не падают на этапе разбора
|
|
911
|
+
с `Can't resolve 'node:fs'`.
|
|
912
|
+
|
|
913
|
+
| Сборщик | Нужна настройка? | Ресурс wasm |
|
|
914
|
+
|---|---|---|
|
|
915
|
+
| **Vite 5** | нет | автоматически попадает в вывод (`assets/*.wasm`) |
|
|
916
|
+
| **Webpack 5** | нет | автоматически выдаётся как asset |
|
|
917
|
+
| **Rspack 2** | нет | выдаётся как asset (имена с hash) |
|
|
918
|
+
| **Turbopack** (по умолчанию в Next.js 16) | нет | попадает в `static/media/*.wasm` |
|
|
919
|
+
| **esbuild** | одна строка плагина (ниже) | esbuild не обрабатывает файлы, на которые указывает `new URL(..., import.meta.url)` |
|
|
920
|
+
| **Node 18+** | нет | читает `dist/<algo>/<algo>.wasm` |
|
|
921
|
+
|
|
922
|
+
Пользователи esbuild подключают небольшой встроенный плагин — он кладёт wasm туда,
|
|
923
|
+
откуда его берёт рантайм:
|
|
924
|
+
|
|
925
|
+
```ts
|
|
926
|
+
import { smCryptoWasm } from 'sm-crypto-wasm/esbuild-plugin';
|
|
927
|
+
|
|
928
|
+
await esbuild.build({
|
|
929
|
+
entryPoints: ['src/main.ts'],
|
|
930
|
+
bundle: true,
|
|
931
|
+
platform: 'browser',
|
|
932
|
+
format: 'esm',
|
|
933
|
+
outdir: 'dist/assets', // сюда попадает выходной JS
|
|
934
|
+
plugins: [smCryptoWasm()], // → wasm копируется в dist/<algo>/<algo>.wasm
|
|
935
|
+
});
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
Не хотите плагин? Скопируйте `node_modules/sm-crypto-wasm/dist/{sm2,sm3,sm4,sm9,zuc}`
|
|
939
|
+
в **родительский** каталог выходного JS (в примере выше — `dist/`).
|
|
940
|
+
|
|
941
|
+
Браузеру нужен **защищённый контекст** (`https://` или `localhost`) для
|
|
942
|
+
`crypto.getRandomValues`; если его нет, библиотека **бросает ошибку** (fail-closed), а не
|
|
943
|
+
откатывается к некриптографическому источнику.
|
|
944
|
+
|
|
945
|
+
Среды со строго потоковой загрузкой WASM, такие как Cloudflare Workers / Vercel Edge,
|
|
946
|
+
**вне области поддержки**; загрузка «сырого» ESM напрямую через `<script type="module">`
|
|
947
|
+
(без сборщика) — тоже.
|
|
1212
948
|
|
|
1213
949
|
---
|
|
1214
950
|
|