WebAssembly加密:浏览器中接近原生的加密速度
使用WebAssembly在浏览器中实现接近原生的加密速度。比较WASM加密实现。
WASM 编译的加密库在浏览器中运行速度约为原生 C 速度的 60–80%,比纯 JavaScript 实现快 3–10 倍。对于 Web Crypto API 不支持的加密原语(ChaCha20-Poly1305、Argon2id、XChaCha20、Kyber 等后量子密钥封装机制),WASM 是达到可用性能的实际路径。libsodium.js 是主流选择,提供完整的 libsodium API,WASM 二进制文件仅 200 KB。对于浏览器原生支持的 AES-256-GCM,硬件加速的内置实现远胜 WASM,因此 WASM 并非万能工具。本文介绍何时选择 WASM 以及如何对比其权衡。
原生 Web Crypto 占优的场景
对于 Web Crypto 已支持的算法——AES-GCM、AES-CBC、AES-CTR、PBKDF2、HMAC、RSA-OAEP、ECDH、ECDSA、SHA-256/384/512——浏览器原生实现在 x86 上借助 AES-NI,在移动端借助 ARMv8 加密扩展进行硬件加速。AES-256-GCM 典型吞吐量对比:
- Web Crypto(Apple Silicon 上的 Chrome):1.7 GB/s
- Web Crypto(带 AES-NI 的 Intel x86 Chrome):1.2 GB/s
- libsodium WASM AES-GCM:400–800 MB/s
- 原生 OpenSSL 参考值:3–5 GB/s
WASM 在沙箱内无法访问 AES-NI,回退至位切片(bitsliced)AES 实现——速度较慢,但保证恒定时间。凡是 Web Crypto 支持的算法,一律优先使用原生实现。
WASM 占优的场景
对于 Web Crypto 不支持的算法:
- ChaCha20-Poly1305:在没有 AES-NI 的设备上比 AES 更快,2026 年浏览器仍无原生支持。
- XChaCha20-Poly1305:192 位 nonce 使随机 nonce 在任何规模下均安全。
- Argon2id:PHC 获奖密码散列函数,浏览器无原生支持。
- X25519 / Ed25519:Safari 17 和 Firefox 129 已添加原生支持,但 WASM 仍是可移植路径。
- BLAKE2b / BLAKE3:Web Crypto 不包含的快速散列函数。
- Kyber、Dilithium、SPHINCS+:后量子原语,只能依赖库实现。
- 流式 AEAD:libsodium 的
crypto_secretstream为大文件流式加密提供了简洁的接口,Web Crypto 没有等效实现。
对于这些算法,JavaScript 实现比 WASM 慢 5–20 倍。在 1 GB 文件上使用 ChaCha20-Poly1305,WASM 约需 2 秒,而纯 JS 需要 15–40 秒。
libsodium.js:主力库
libsodium-wrappers(通过 Emscripten 构建的 libsodium 及 JS 封装)是首选库。200 KB WASM + 约 50 KB JS 封装,gzip 压缩后更小。
import _sodium from 'libsodium-wrappers';
await _sodium.ready;
const sodium = _sodium;
// ChaCha20-Poly1305 认证加密
const key = sodium.crypto_aead_xchacha20poly1305_ietf_keygen();
const nonce = sodium.randombytes_buf(
sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES
);
const ciphertext = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
plaintext, null, null, nonce, key
);
"sumo" 构建版本(包含更多算法)约为 600 KB;默认构建覆盖 90% 的使用场景,包括 AEAD、密码散列(Argon2id)、X25519 和 Ed25519。
动态加载以避免 WASM 二进制阻塞初始页面渲染:
async function getSodium() {
if (!window._sodium) {
const mod = await import('libsodium-wrappers');
await mod.ready;
window._sodium = mod;
}
return window._sodium;
}
通过 crypto_secretstream 实现流式 AEAD
对于大文件传输,libsodium 的 crypto_secretstream_xchacha20poly1305 是浏览器中最简洁的流式 AEAD 方案:
const { state, header } =
sodium.crypto_secretstream_xchacha20poly1305_init_push(key);
const chunk1 = sodium.crypto_secretstream_xchacha20poly1305_push(
state, plaintext1, null,
sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE
);
// 最后一块使用 TAG_FINAL,让接收方检测截断攻击
const chunkLast = sodium.crypto_secretstream_xchacha20poly1305_push(
state, plaintextLast, null,
sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
);
TAG_FINAL 标记让接收方可检测截断攻击——若攻击者丢弃尾部块,客户端解密即告失败。Web Crypto 的 AES-GCM 没有这种机制,需要自行构建截断检测(在 AAD 中记录块数,或对整个文件散列值进行后置校验)。
浏览器中的 Argon2
对于浏览器端基于密码的密钥派生,Argon2id via WASM 是 2026 年的最佳实践。可选库:
- argon2-browser:专用 Argon2 WASM 库,约 200 KB
- libsodium.js:通过
crypto_pwhash提供 Argon2id(如已使用 libsodium) - @noble/hashes:纯 JS Argon2id,约 15 KB,比 WASM 慢 3–5 倍
示例(argon2-browser):
import argon2 from 'argon2-browser';
const result = await argon2.hash({
pass: password,
salt: salt, // Uint8Array,16+ 字节
type: argon2.ArgonType.Argon2id,
time: 3,
mem: 65536, // KiB,即 64 MiB
parallelism: 4,
hashLen: 32,
});
// result.hash 是可直接用作 AES-256 密钥的 Uint8Array
根据目标用户等待时间校准参数。OWASP 2024 基线(m=19 MiB, t=2, p=1)运行约 300–500 ms;更高强度(m=64 MiB, t=3, p=4)在现代硬件上约需 1–2 秒。
通过 WASM 实现后量子密码
Kyber(密钥封装)和 Dilithium(签名)于 2024 年由 NIST 标准化为 ML-KEM 和 ML-DSA。JavaScript 实现虽然存在,但 liboqs-js 等 WASM 版本更快、可审计性更强。
对于文件传输,后量子 KEM 允许使用量子安全公钥原语封装对称文件密钥。混合方案(ML-KEM + X25519)同时防御经典攻击者和未来量子攻击者。Chrome 在版本 116(2023 年)就已在 TLS 握手中启用 ML-KEM,但应用层 WASM 仍是文件内容密钥的实现路径。
包体积考量
WASM 二进制文件作为 JS 包的一部分发布,或按需延迟加载。大致体积(gzip 后):
- libsodium.js 默认版:200 KB
- libsodium.js sumo 版:600 KB
- argon2-browser:200 KB
- liboqs-js(后量子):1 MB+
对于以浏览器端加密为主要功能的应用,在首次加载时包含 WASM 是合理的。使用 rel="modulepreload" 预加载提示或 Service Worker 缓存,确保后续访问即时加载。在浏览器开发者工具网络面板中确认 WASM 二进制文件在首次加载后已被缓存——缓存头配置错误会导致每次访问都重新下载。
编译与实例化开销
WebAssembly.instantiate() 解析并编译二进制文件,200 KB 模块在桌面端耗时 20–100 ms,在移动端耗时 100–500 ms,每个会话仅发生一次。可通过 IndexedDB 缓存编译后的 WebAssembly.Module 以加快后续加载。
WebAssembly.instantiateStreaming() 将下载与编译流水线化,相比先获取再实例化可节省 30–50% 的启动时间:
const response = fetch('/sodium.wasm');
const { instance } = await WebAssembly.instantiateStreaming(
response, importObject
);
内存管理注意事项
WASM 模块以 64 KiB 为单位的线性页面分配内存。libsodium 默认从小内存起步按需增长,但无限制增长可能触及浏览器限制(32 位 WASM 通常为 4 GB 线性内存上限)。对于多 GB 文件加密:
- 以流的方式将块传递给 WASM 模块,不要将整个文件加载到 WASM 内存
- 处理完每块后通过
sodium.memzero或置空引用来释放缓冲区 - 在长时间运行的会话中监控
WebAssembly.Memory.buffer.byteLength
分块处理是目前最可靠的路径。
实践方案总结
2026 年文件传输应用的推荐做法:
- 使用 Web Crypto 处理 AES-256-GCM、PBKDF2、HMAC、SHA-256,以及需要时的 RSA-OAEP
- 使用 libsodium.js via WASM 进行 Argon2id 密码散列
- 使用 libsodium.js 对大文件进行流式加密(
crypto_secretstream) - 在支持的浏览器上使用原生 Ed25519/X25519,否则回退到 libsodium
- 用户交互后再延迟加载 WASM,保持初始包体积轻量
- 在目标设备上实际基准测试,不要用桌面数据套用移动端
HexaTransfer 对文件批量加密坚持使用 Web Crypto AES-GCM,因为硬件加速路径是最快的。WASM 仅保留给原生 API 无法提供的算法。
在 hexatransfer.com 上试试 — 免费、无需注册、最多 10 GB。