导语:浏览器密码学的第三种力量
浏览器早就内置了密码学——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 的边界与限制
- 2. 何时需要 WASM 密码库
- 3. libsodium 与 OpenSSL 编译到 WASM
- 4. constant-time 与侧信道防护
- 5. 密钥管理与安全存储
- 6. MPC 与零知识证明在 WASM
- 7. 与 native crypto 的性能对比
- 8. 浏览器密码学实战:加密、签名与哈希
- 9. 选型矩阵与审计建议
- 10. 速查表与一句话记忆
- 延伸阅读
1. WebCrypto API 的边界与限制
WebCrypto 是浏览器内置的原生密码接口,性能好、有硬件加速,但边界清晰:
| WebCrypto 有的 | WebCrypto 没有的 |
|---|---|
| AES-CBC/GCM/CTR、RSA-OAEP | Argon2、scrypt、bcrypt |
| ECDSA/ECDH、RSA-PSS | X25519/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 密码库
| 触发信号 | 例子 |
|---|---|
| 算法不在 WebCrypto | Argon2 口令哈希、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.6x | WebCrypto 碾压,别用 WASM |
| SHA-256 | 最快(硬件加速) | 0.4~0.7x | WebCrypto 碾压 |
| 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 / ECDSA | WebCrypto(原生最快) |
| Argon2 / scrypt | WASM(hash-wasm / libsodium) |
| X25519 / Ed25519 | WASM(libsodium),浏览器支持不稳定 |
| 端到端加密协议 | libsodium secretstream / crypto_box |
| 门限签名 / ZK | WASM(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/ZK | WASM 提供接近原生速度 + 沙箱隔离 |
| 性能 | 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 对照
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。