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 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 压缩函数、分组密码轮函数)在 WASM 内执行。下表覆盖本库
42
- 每个算法的**全部公开操作**;`gmkitx` 一列是对应的纯 JS 参考实现(同机、同参数、
43
- 同消息),**它没有的操作留 `—`**。实测环境 AMD Ryzen 7 7800X3D / Node 24.15 / Windows 11,
44
- `pnpm bench` 可复现:
45
-
46
- > **关于 SM2 一行**:这一层是 TS 门面 API(签名走公钥派生缓存,加密每次仍要做 hex 与
47
- > 曲线点编解码、并调用宿主 CSPRNG),实测在多次运行之间波动可达 ±20%,远大于
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
- | 浏览器 / Vite / Webpack | `fetch(new URL('./<algo>.wasm', import.meta.url))`,打包器负责重写 URL |
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
- Cloudflare Workers / Vercel Edge 这类"严格流式 WASM"环境**不在当前范围**。
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) run
427
- inside WASM. The table below covers **every public operation of every algorithm**
428
- in this library; the `gmkitx` column is the corresponding pure-JS reference (same
429
- machine, same parameters, same messages), and **operations it does not offer are
430
- left as `—`**. Measured on AMD Ryzen 7 7800X3D / Node 24.15 / Windows 11, reproducible
431
- with `pnpm bench`:
432
-
433
- | Algorithm | Operation | sm-crypto-wasm | pure-JS reference (gmkitx) | |
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 | Notes |
538
+ | Environment | Loading path |
744
539
  |---|---|
745
- | Node 18+ | reads `dist/<algo>/<algo>.wasm` via `fs/promises` |
746
- | Browser / Vite / Webpack | `fetch(new URL('./<algo>.wasm', import.meta.url))`; the bundler rewrites the URL |
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
- Strict-streaming WASM environments such as Cloudflare Workers / Vercel Edge
751
- are **out of scope**.
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
- шифра) выполняются внутри WASM. Таблица ниже покрывает **все публичные операции
887
- каждого алгоритма**; столбец `gmkitx` — соответствующая чистая JS-реализация
888
- (та же машина, те же параметры, те же сообщения), а **операции, которых у неё
889
- нет, оставлены как `—`**. Измерено на AMD Ryzen 7 7800X3D / Node 24.15 / Windows 11,
890
- воспроизводится через `pnpm bench`:
891
-
892
- | Алгоритм | Операция | sm-crypto-wasm | чистая JS (gmkitx) | |
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
- | Браузер / Vite / Webpack | `fetch(new URL('./<algo>.wasm', import.meta.url))`; бандлер переписывает URL |
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
- Среды со строго потоковой загрузкой WASM, такие как Cloudflare Workers /
1211
- Vercel Edge, **вне области поддержки**.
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