WASM 密码学:WebCrypto、WASM 密码库与安全计算

详解 WASM 密码学:WebCrypto API 的边界与限制、何时需要 WASM 密码库、libsodium 与 OpenSSL 编译到 WASM、constant-time 与侧信道防护、密钥管理与安全存储、MPC/零知识证明在 WASM、与 native crypto 的性能对比及浏览器密码学实战。

导语:浏览器密码学的第三种力量

浏览器早就内置了密码学——WebCrypto API 直接调用浏览器底层原生实现,快且安全,但算法清单是「官方固定」的:没有 Argon2、没有 XChaCha20、部分浏览器缺 Ed25519。当应用需要新算法、自建协议、敏感的多方计算时,WASM 成为第三种力量:把 libsodium、OpenSSL 乃至零知识证明库编译进浏览器,算法与实现完全自控。本文系统讲 WASM 密码学:WebCrypto 边界、何时上 WASM、libsodium/OpenSSL 编译、constant-time 侧信道防护、密钥管理、MPC/ZK 在 WASM,以及与 native crypto 的性能对比。

前置:/wasm-security-sandbox/(安全沙箱)、/wasm-javascript-interop/(JS 互操作)、/wasm-binary-format-memory-model/(内存模型)。


目录


1. WebCrypto API 的边界与限制

WebCrypto 是浏览器内置的原生密码接口,性能好、有硬件加速,但边界清晰:

WebCrypto 有的WebCrypto 没有的
AES-CBC/GCM/CTR、RSA-OAEPArgon2、scrypt、bcrypt
ECDSA/ECDH、RSA-PSSX25519/Ed25519(部分浏览器)、XChaCha20
SHA-1/2、HMAC带自定义参数的 KDF 派生
PBKDF2、HKDF流式对称加密接口(一次一块)
CryptoKey 对象模型自定义 AEAD 协议、门限签名

关键限制:算法集由浏览器决定、随浏览器版本漂移;密钥材料必须是 CryptoKey 对象(无法直接把任意字节当 AES 密钥,要先 importKey);subtle 方法全是异步 Promise。

// WebCrypto 基础姿势
const key = await crypto.subtle.importKey(
  'raw', rawKeyBytes, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']
);
const ct = await crypto.subtle.encrypt(
  { name: 'AES-GCM', iv: new Uint8Array(12) }, key, plaintext
);

一句话总结:WebCrypto = 浏览器原生、硬件加速、算法清单固定——新算法、自定义协议、任意密钥字节的场景都要绕开它。


2. 何时需要 WASM 密码库

触发信号例子
算法不在 WebCryptoArgon2 口令哈希、XChaCha20-Poly1305
需要自建协议端到端加密消息(crypto_box/secretstream)
复用现有生态OpenSSL、libsodium、RustCrypto 的算法
多方计算门限签名、私密求交、零知识证明
一致的跨端实现浏览器/Node/边缘跑同一份字节码

反例——能用 WebCrypto 就别上 WASM:AES-GCM 加解密、RSA/ECDSA 签名、SHA-2 哈希、HKDF 派生,WebCrypto 原生更快更省内存;WASM 只补 WebCrypto 的「能力缺口」。

一句话总结:WASM 密码库的适用标准是「WebCrypto 干不了或生态要复用」——新算法、自建协议、MPC/ZK 上 WASM,常规算法留在 WebCrypto。


3. libsodium 与 OpenSSL 编译到 WASM

3.1 libsodium.js:最常用的 WASM 密码库

libsodium 是现代密码算法集,官方支持编译到 WASM(libsodium.js / libsodium-wrappers):

// crypto_box:非对称加密(X25519 + XSalsa20 + Poly1305)
import sodium from 'libsodium-wrappers-sumo';
await sodium.ready;
const bob = sodium.crypto_box_keypair(), alice = sodium.crypto_box_keypair();
const nonce = sodium.randombytes_buf(sodium.crypto_box_NONCEBYTES);
const ciphertext = sodium.crypto_box_easy(msg, nonce, bob.publicKey, alice.privateKey);
const plain = sodium.crypto_box_open_easy(ciphertext, nonce, alice.publicKey, bob.privateKey);

3.2 自行编译 libsodium / OpenSSL

# libsodium 编译 WASM(官方 makefile 支持)
cd libsodium && emmake make -j8 \
  CFLAGS="-s WASM=1 -s ALLOW_MEMORY_GROWTH=1" \
  && cp dist/libsodium-wasm32.wasm ../../dist/

# OpenSSL 编译到 WASM(重,一般用 BoringSSL/RustCrypto 替代)
emconfigure ./Configure -DOPENSSL_SMALL \
  -Wl,--no-entry -s EXPORTED_FUNCTIONS=_EVP_EncryptUpdate,...

现实建议:OpenSSL 体积大、依赖多,浏览器场景通常选 libsodium(算法现代、API 简洁)或 RustCrypto(经 wasm-bindgen 暴露)。

一句话总结:libsodium.js 开箱即用、官方 WASM 构建;OpenSSL 太重,浏览器优先 libsodium/RustCrypto——「算法现代 + 构建省事」是选型主轴。


4. constant-time 与侧信道防护

密码学代码的头号杀手是时间侧信道:如果代码分支或内存访问依赖密钥/秘密值,攻击者用大量测量即可恢复秘密。WASM 沙箱不自动防御这类攻击,实现要刻意做到 constant-time:

□ 禁止:if (secret_bit) {...}  /  array[secret_value] —— 分支与寻址依赖秘密
□ 允许:位运算掩码(select),如 (a & -flag) | (b & ~flag)
□ 必须:常量时间比较,如 crypto_verify_32(a, b)(libsodium 提供)
// 反例:分支比较(长度与内容时序泄露);正例:XOR 累积
function badEq(a, b) { for (let i = 0; i < a.length; i++)
  if (a[i] !== b[i]) return false; return true; }        // 不恒定时间
function constEq(a, b) { let d = 0;
  for (let i = 0; i < a.length; i++) d |= a[i] ^ b[i]; return d === 0; }  // 恒定时间

审计建议:优先用久经验证的库实现(libsodium/RustCrypto),自己写的「密码学代码」默认不信任;对秘密值的所有分支与索引做 grep 审计。

一句话总结:constant-time = 秘密值不参与分支、不决定内存地址、比较用 XOR 累积——自己写的密码代码默认不可信,优先复用审计过的库。


5. 密钥管理与安全存储

5.1 浏览器里密钥怎么存

□ WebCrypto CryptoKey:extractable=false 不可导出,可 structured-clone 进 IndexedDB
□ WASM 原始密钥:只能加密落盘,用「主密钥 + 包裹密钥 KEK」两层结构
□ 口令派生:PBKDF2/HKDF(WebCrypto)或 Argon2(WASM)
// 两层密钥:主密钥加密存储,包裹密钥(KEK)由口令派生
const kek = await crypto.subtle.importKey(
  'raw', await crypto.subtle.digest('SHA-256', passphrase), // 简化示意
  { name: 'AES-GCM' }, false, ['wrapKey', 'unwrapKey']
);
const wrapped = await crypto.subtle.wrapKey(
  'raw', dataKey, kek, { name: 'AES-GCM', iv }
);

5.2 密钥生命周期

生成(CSPRNG:crypto.getRandomValues / randombytes)→ 使用 → 轮换 → 销毁
□ 不可导出密钥永不离开 CryptoKey 对象;轮换加版本号 + 双密钥并行期
□ 销毁:WASM 内 memset 清零,JS 侧置 null 并等待 GC

一句话总结:密钥管理 = WebCrypto 不可导出 CryptoKey + 两层包裹 + 明确生命周期;WASM 库的原始密钥必须加密落盘,销毁时显式清零。


6. MPC 与零知识证明在 WASM

MPC(多方计算)与零知识证明(ZK)是计算密集、算法新、协议自定义的典型——正是 WebCrypto 覆盖不到、WASM 大显身手的领域:

□ 门限签名:n 方共享密钥,t 方即可签名(如 FROST)
□ 私密求交(PSI):双方算交集而不泄露非交集
□ 零知识证明:证明「我知道 x 满足关系 R」而不泄露 x
  (zk-SNARK/STARK、Pedersen 承诺、Bulletproofs)
□ 同态加密:对密文直接做加法/乘法
// 浏览器端生成零知识证明(示意,如 snarkjs)
import * as snarkjs from 'snarkjs';
const proof = await snarkjs.groth16.prove(wasmFile, zkeyFile, witness);
const ok = await snarkjs.groth16.verify(vkey, proof, publicInputs);

WASM 的价值:把复杂的椭圆曲线运算(BN254/Bls12-381)以接近原生速度跑在浏览器/边缘,配合沙箱隔离多方计算的密钥材料。

一句话总结:MPC/ZK 在 WASM = 算法新、协议自定义、计算密集,WASM 提供接近原生的速度与沙箱隔离,是门限签名与零知识证明在终端的唯一现实路径。


7. 与 native crypto 的性能对比

操作WebCrypto(原生)libsodium WASM说明
AES-GCM 加解密最快(硬件 AES-NI)0.3~0.6xWebCrypto 碾压,别用 WASM
SHA-256最快(硬件加速)0.4~0.7xWebCrypto 碾压
X25519无(部分浏览器有)~0.5x native只能 WASM,性能足够
Argon2无~0.5x native只能 WASM
Ed25519 签名部分浏览器~0.4x native只能用 WASM 保持一致

结论:WASM 密码 ≈ native 的 30%~60%,对大多数应用绰绰有余;真正不能接受差距的场景(海量数据 AES 加密)就用 WebCrypto。WASM + SIMD 还能把 ChaCha20 等向量化算法追到接近原生。

一句话总结:WASM 密码 ≈ native 30-60%,SIMD 可再追——WebCrypto 覆盖的算法别浪费,WebCrypto 覆盖不到的新算法 WASM 性能完全够用。


8. 浏览器密码学实战:加密、签名与哈希

8.1 端到端加密消息(libsodium secretstream)

// 加密流:大消息分块、防重放、密文自认证
const sk = sodium.crypto_secretstream_xchacha20poly1305_keygen();
const [stateOut, header] = sodium.crypto_secretstream_xchacha20poly1305_init_push(sk);
const ct1 = sodium.crypto_secretstream_xchacha20poly1305_push(stateOut, chunk1, null, 0);
const ct2 = sodium.crypto_secretstream_xchacha20poly1305_push(stateOut, chunk2, null, 1); // 末块

8.2 哈希与口令

// 快速哈希(SHA-256 via WebCrypto)
const digest = await crypto.subtle.digest('SHA-256', data);
// 口令哈希(Argon2 via WASM,抗 GPU 爆破)
import { argon2id } from 'hash-wasm';
const hash = await argon2id({ password, salt, parallelism: 4, iterations: 3, memorySize: 65536 });

一句话总结:实战组合 = WebCrypto 覆盖常规算法 + WASM 补 Argon2/XChaCha20/Ed25519——口令存 Argon2id(绝不明文/MD5)、消息加密用 AEAD、协商签名用现代曲线。


9. 选型矩阵与审计建议

需求选择
AES-GCM / SHA-2 / RSA / ECDSAWebCrypto(原生最快)
Argon2 / scryptWASM(hash-wasm / libsodium)
X25519 / Ed25519WASM(libsodium),浏览器支持不稳定
端到端加密协议libsodium secretstream / crypto_box
门限签名 / ZKWASM(FROST / snarkjs)
常量时间原语libsodium(crypto_verify_32 等)
审计清单:
□ 只依赖审计过的库(libsodium/RustCrypto),不手写原语
□ secret 值 grep 审计:分支、索引、日志都不能碰
□ CSPRNG:crypto.getRandomValues / sodium.randombytes_buf
□ 密钥不可导出 + 加密落盘 + 明确轮换
□ 算法版本与参数写死在协议里,别让调用方随意选

一句话总结:选型 = WebCrypto 打底 + WASM 补缺口 + 审计过的库——不手写密码、秘密不碰分支、随机数只用 CSPRNG、参数写死进协议。


10. 速查表与一句话记忆

问题一句话答案
WebCrypto 边界原生快但算法清单固定、密钥须 CryptoKey
何时上 WASM新算法 / 自建协议 / 复用生态 / MPC-ZK
首选库libsodium.js(官方 WASM 构建)
OpenSSL太重,浏览器用 libsodium/RustCrypto
constant-time秘密不碰分支/索引,比较用 XOR 累积
密钥存储不可导出 CryptoKey + 两层包裹 + 加密落盘
MPC/ZKWASM 提供接近原生速度 + 沙箱隔离
性能WASM ≈ native 30-60%,SIMD 可追
实战WebCrypto 常规 + WASM 补 Argon2/XChaCha/Ed25519
审计只用审计库 + grep 秘密 + CSPRNG + 参数写死

一句话记忆:WASM 密码学 = WebCrypto 打底、WASM 补缺口——WebCrypto 原生快但算法清单固定(没有 Argon2/XChaCha20/Ed25519),新算法、自建协议、MPC/零知识就上 WASM;首选 libsodium.js 官方构建,OpenSSL 太重;constant-time 铁律是「秘密不碰分支、不决定索引、比较用 XOR 累积」;密钥管理 = 不可导出 CryptoKey + 两层包裹 + 加密落盘 + 轮换销毁;WASM 密码 ≈ native 30-60% 完全够用——「只用审计过的库,别手写密码原语」。


延伸阅读

  • /wasm-security-sandbox/ — WASM 安全沙箱模型
  • /wasm-javascript-interop/ — JS 互操作与胶水层
  • /wasm-binary-format-memory-model/ — 内存模型与数据搬运
  • /wasm-simd-high-performance/ — SIMD 加速密码运算
  • /wasm-wasmtime-runtime/ — Wasmtime 服务端运行
  • [[nodejs]] — Node.js 的 WebCrypto 对照

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「wasm」更多文章

  1. WASM 构建与打包工具链:wasm-bindgen、wasm-pack 与前端集成
  2. WASM 媒体处理:音视频编解码、转码与滤镜
  3. WASM 数据库与持久化:SQLite、OPFS 与本地存储