Web Crypto API指南:开发者的浏览器原生加密
掌握Web Crypto API构建加密文件传输应用。AES-GCM、RSA-OAEP和浏览器密钥管理完整指南。
Web Crypto API(由 W3C Web 加密 API 规范定义,通过 window.crypto.subtle 暴露)是浏览器原生执行密码学运算的方式,无需通过网络加载额外的加密库。它支持 AES-GCM、AES-CBC、AES-CTR、AES-KW、HMAC、RSA-OAEP、RSA-PSS、RSASSA-PKCS1-v1_5、ECDH、ECDSA、HKDF 和 PBKDF2,覆盖所有现代浏览器(Chrome 37+、Firefox 34+、Safari 10.1+、Edge 79+)。对于文件传输应用,这意味着每个字节的密文都可以在上传之前在客户端生成,浏览器提供经过审计的恒定时间实现。本文介绍文件加密传输中关键的算法原语,以及每次首次实现都会踩到的坑。
SubtleCrypto 是基于 Promise 的异步 API
crypto.subtle 上的每个方法都返回 Promise。这是有意为之:加密操作可以卸载到硬件或后台线程,强制异步防止 API 被以阻塞主线程的方式误用。代码形式:
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true, // extractable
["encrypt", "decrypt"]
);
第二个参数(true)将密钥标记为可提取,意味着后续可以通过 crypto.subtle.exportKey() 导出。对于长期密钥,设置为 false 可使原始字节无法被 JavaScript 访问。对于需要序列化到 URL 片段(HexaTransfer 模式)的密钥,设置为 true。
第三个参数是密钥用途数组。以 ["encrypt"] 生成的密钥即使 AES-GCM 是对称算法,也无法用于解密。这种分离可防止被入侵的加密流程被滥用来解密历史数据。
AES-GCM:对称文件加密的核心算法
AES-GCM 是文件内容加密的主力算法。它提供带关联数据的认证加密(AEAD):密文加上认证标签,以及可选的已认证但未加密的关联数据。文件传输使用 256 位密钥,按 NIST SP 800-38D 建议使用 96 位 nonce。
const iv = crypto.getRandomValues(new Uint8Array(12)); // 96 位 nonce
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
plaintext
);
输出中包含附加在密文后的 128 位 GCM 认证标签。解密时自动验证标签,不匹配则抛出异常。绝不能在同一密钥下重用 nonce;GCM 在 nonce 重用时安全性会灾难性崩溃(攻击者可以恢复认证密钥)。对于每个文件使用新密钥的文件传输,随机 nonce 是安全的;对于长期密钥,使用计数器。
PBKDF2:从密码派生密钥
当用户输入密码保护文件时,不能直接将密码用作 AES 密钥。先通过 PBKDF2 处理:
const passwordKey = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
"PBKDF2",
false,
["deriveKey"]
);
const aesKey = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: crypto.getRandomValues(new Uint8Array(16)),
iterations: 600000,
hash: "SHA-256",
},
passwordKey,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
OWASP 2023 密码哈希指南建议 PBKDF2-SHA-256 使用 60 万次迭代。低于 31 万次低于当前最佳实践。盐值必须随机,并与密文一起存储(非保密,但必须唯一)。
对于 2026 年的新代码,考虑用 Argon2id 替代 PBKDF2。Argon2 尚未纳入 Web Crypto API,但 argon2-browser 或 @noble/hashes 等库提供 JavaScript/WASM 实现。Argon2id 抵抗 GPU 攻击的能力远超 PBKDF2。
RSA-OAEP:密钥封装
对于需要用接收者公钥加密文件 AES 密钥的场景,使用 RSA-OAEP。生成密钥对:
const keyPair = await crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]), // 65537
hash: "SHA-256",
},
true,
["encrypt", "decrypt"]
);
新密钥使用 4096 位模长;2048 位仍然安全,但随着量子计算时间线的明确,正在逐步被弃用。RSA-OAEP 只能加密小载荷(最多 modulusLength/8 - 2*hashLength - 2 字节),因此用它封装 256 位 AES 密钥,而非直接加密文件内容。
对于性能敏感的应用,使用 P-256 或 P-384 的 ECDH 是优于 RSA 的替代方案。密钥生成速度快一个数量级,密钥尺寸也小得多。
大文件的流式处理
2 GB 的文件不适合放入浏览器的 ArrayBuffer。Chrome、Firefox 和 Safari 都允许通过 File.stream() 返回 ReadableStream 来读取文件,然后分块处理。Web Crypto API 本身尚没有流式加密/解密方法(这是规范中的一个空白),有两种解决方案:
- 拆分为数据块(64 KB 或 1 MB),每块使用唯一 nonce 加密。接收者按顺序拼接。这在整个文件层面失去了真正的 AEAD,但对大多数场景有效。
- 使用 WASM 加密库(libsodium.js、支持 WASM 后端的 @noble/ciphers),它们支持流式 AEAD 模式,如 XChaCha20-Poly1305 或 AES-GCM-SIV。
对于几百兆字节以下的传输,缓冲 AES-GCM 工作良好且更简单。超出此范围后,流式处理就成为必要,以避免内存压力。
密钥导出、导入与 URL 片段
对于密钥在 URL 片段中传递的 HexaTransfer 类流程:
const rawKey = await crypto.subtle.exportKey("raw", aesKey);
const keyBase64 = btoa(String.fromCharCode(...new Uint8Array(rawKey)));
// 共享 URL 如:https://example.com/file/abc123#key=keyBase64
URL 片段在 HTTP 请求中永远不会发送给服务器(浏览器会剥离它们)。这使密钥保持在客户端,即使用户共享了链接。接收者端:
const keyBase64 = window.location.hash.slice(5); // 剥除 "#key="
const rawKey = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
const key = await crypto.subtle.importKey(
"raw", rawKey, "AES-GCM", false, ["decrypt"]
);
使用 base64url 编码(将 + 替换为 -,/ 替换为 _,去掉填充),以避免 URL 编码问题。
常见陷阱
使用 Math.random() 生成盐值或 IV。 Math.random() 不是密码学安全的随机数生成器。始终使用 crypto.getRandomValues()。
在同一密钥下重用 IV。 GCM 在 nonce 重用时安全属性完全崩溃。96 位随机 nonce 在同一密钥下约 2^48 次加密后发生碰撞(生日界限)。对于每个文件使用自己密钥的文件传输是安全的;对于长期密钥使用计数器。
忘记启用 HTTPS。 crypto.subtle 仅在安全上下文(HTTPS 或 localhost)中可用。在不安全来源上,crypto.subtle 是 undefined。
在 IndexedDB 中不加保护地存储可提取密钥。 如果需要持久化密钥,在存储之前先封装(例如用密码派生密钥)。永远不要将原始 AES 密钥存储在 localStorage 中,它可被该来源的任意脚本访问。
不经 PBKDF2 直接信任用户提供的密码。 转换为 UTF-8 字节的原始密码不是 256 位密钥。始终进行派生。
不验证认证标签。 crypto.subtle.decrypt() 对 AES-GCM 会自动执行此验证,但如果在其上实现自定义协议,不要跳过检查。
浏览器支持的细节差异
所有主流浏览器在 HTTPS 下均支持 Web Crypto。一些特殊情况:
- Safari 的 PBKDF2 多年来慢于 Chrome/Firefox;差距在 Safari 15 中已缩小。
- Firefox 执行更严格的输入验证;在 Chrome 中运行的代码可能在 Firefox 中抛出
OperationError。两者都需要测试。 - Service Worker 中的 Web Crypto 可以工作,但要求注册范围为 HTTPS。
- Node.js 自 Node 15 起通过
require("crypto").webcrypto提供兼容 API,适用于同构加密代码。
何时改用库
Web Crypto 的基础功能完善,但缺乏 ChaCha20-Poly1305、Argon2、X25519 和 Ed25519 等现代算法原语(不过 Ed25519 正在引入)。对于这些,libsodium.js(通过 WASM)或 @noble/ciphers / @noble/curves(纯 JavaScript,已审计)是领先选项。HexaTransfer 直接使用 Web Crypto 原语进行 AES-GCM 和 PBKDF2,因为它们涵盖了文件传输路径,无需额外依赖。
一个完整的加密传输流程,用 Web Crypto 不到 100 行代码即可实现:生成 AES 密钥,派生或随机,加密文件,上传密文,共享密钥在片段中的链接,接收者导入密钥并解密。仅此而已。
在 hexatransfer.com 上试试 — 免费、无需注册、最多 10 GB。