2012年12月,第29届混沌通信大会(29C3)在汉堡举行。Harry Halpin在演讲中抛出一个颇具挑衅意味的标题——“Re-igniting the Crypto Wars on the Web”(在Web上重新点燃密码学战争)。这不是耸人听闻。当时的背景是:斯诺登事件尚未爆发,但全球范围内的网络监控已成公开的秘密。Web应用亟需端到端加密能力,而JavaScript却在这方面显得力不从心。
五年后,2017年1月26日,W3C正式发布Web Cryptography API推荐标准。这个看似枯燥的技术规范,实际上代表着浏览器安全架构的一次根本性重构。它的核心创新不是提供加密算法——任何JavaScript库都能做到这一点——而是重新定义了密钥的存在方式:密钥不再作为JavaScript对象存在于内存中,而是存储在浏览器管理的独立空间里,JavaScript代码只能获得一个opaque reference(不透明引用)。
这个设计选择背后的逻辑链条,以及由此衍生的安全边界与限制,值得每一个Web开发者深入理解。
JavaScript加密的根本困境
理解Web Crypto API的设计动机,需要先回到一个问题:为什么JavaScript不能安全地进行加密操作?
这不是算法问题。AES-GCM、RSA-OAEP、Ed25519——这些算法的数学原理与实现语言无关。问题出在JavaScript的执行环境本身。
2011年,Thomas Ptacek写了一篇题为"JavaScript Cryptography Considered Harmful"的文章,系统性地阐述了JavaScript加密的脆弱性。核心论点可以归纳为:
随机数生成的悖论。密码学安全的随机数需要从操作系统的高熵源获取,如/dev/urandom。但JavaScript最初没有访问这些源的能力,Math.random()只是一个伪随机数生成器,其熵值远不足以支撑密码学用途。即使后来引入了crypto.getRandomValues(),这个同步API在某些场景下仍然存在时序攻击的风险。
密钥的内存困境。在JavaScript中,密钥本质上是一个Uint8Array或字符串,存储在堆内存中。垃圾回收器何时回收它、回收后是否真正擦除、是否被交换到磁盘——这些都不受开发者控制。XSS攻击一旦成功,攻击者可以通过JSON.stringify()或简单的内存遍历获取密钥明文。
供应链攻击面。使用第三方加密库意味着信任整个依赖链。npm生态系统中,一个被攻破的维护者账户可以悄无声息地修改加密库的代码,将密钥导出到攻击者控制的服务器。这类攻击已经发生过多次。
WebKit团队在2017年的博客中明确指出:“WebCrypto API instead protects the secret or private keys by storing them completely outside of the JavaScript execution context.“这句话道出了问题的本质——JavaScript执行环境本身就是不可信的。
SubtleCrypto:命名中的哲学
Web Crypto API的核心接口被命名为SubtleCrypto。这个名字并非随意选择。
“Subtle"一词暗示了这个API的两个特性:它是底层(low-level)的,需要开发者对密码学有相当程度的理解;它的操作具有微妙的(subtle)安全含义,使用不当可能导致安全漏洞。
这个命名选择实际上是一个警示:这不是一个"encrypt()然后就安全了"的高级API,而是一组密码学原语(primitives),开发者需要自行组装正确的安全方案。
异步设计的深意
SubtleCrypto的所有方法都返回Promise,都是异步操作。这不是性能优化的选择,而是安全设计的必然。
// 加密操作是异步的
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: iv },
key,
plaintext
);
异步设计确保了密钥操作不会阻塞主线程——但这只是表象。更深层的原因是:密钥操作可能在浏览器的安全隔离区域(Secure Enclave、TPM)中执行,跨越这个边界需要异步通信。
Secure Context的要求
Web Crypto API只能在"Secure Context"中使用,即HTTPS页面或localhost。这个限制不是任性,而是基于以下考量:
- 防止中间人攻击替换加密代码
- 确保Service Worker和Web Worker的安全执行
- 与浏览器的其他安全特性(如Cookie的Secure属性)保持一致
尝试在HTTP页面访问crypto.subtle会得到undefined,这是一个hard fail,而不是运行时错误——设计者希望开发者在开发阶段就发现这个问题。
CryptoKey:不透明的权力
Web Crypto API最关键的设计是CryptoKey对象。理解它,就理解了这个API的一半。
const keyPair = await crypto.subtle.generateKey(
{ name: "ECDSA", namedCurve: "P-256" },
false, // extractable = false
["sign", "verify"]
);
上面这段代码生成了一个ECDSA密钥对。extractable: false意味着这个私钥永远无法被导出为原始字节。即使XSS攻击成功获取了keyPair.privateKey这个对象,攻击者也无法获得密钥的原始值——因为密钥根本不在JavaScript可访问的内存中。
结构化克隆算法与IndexedDB
CryptoKey对象可以被存储在IndexedDB中,这得益于结构化克隆算法(Structured Clone Algorithm)。但这里有一个关键区别:
存储在IndexedDB中的是CryptoKey对象的引用,而不是密钥数据本身。当从IndexedDB读取时,浏览器会验证请求是否来自同一个origin。这个机制确保了:
- 只有创建密钥的origin可以访问它
- 密钥数据永远不会出现在JavaScript堆内存中
- 即使用户清除浏览器数据,密钥也会随之删除
但这并不意味着绝对安全。INRIA的研究人员在2017年的论文中指出了一种攻击方式:如果攻击者能够执行任意JavaScript代码(通过XSS),他虽然无法直接导出密钥,但可以使用这个密钥执行签名或解密操作。extractable: false阻止的是密钥泄露,而不是密钥滥用。
extractable属性的双刃剑
extractable属性的设计存在一个有趣的权衡:
- 设为
true:密钥可以导出,方便备份和迁移,但XSS攻击可能导致密钥泄露 - 设为
false:密钥无法导出,更安全,但用户更换设备或清除浏览器数据后密钥将丢失
这个选择没有标准答案,取决于具体应用场景。一个常见的模式是:
// 会话密钥:extractable: false,用完即弃
const sessionKey = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
// 长期密钥:extractable: true,导出后存储在安全位置
const masterKey = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["wrapKey", "unwrapKey"]
);
密钥包装:被忽视的安全漏洞
wrapKey()和unwrapKey()方法用于密钥的安全传输,但INRIA研究人员发现了一个严重的设计缺陷。
攻击场景
假设开发者使用以下代码包装一个签名密钥:
const wrappedKey = await crypto.subtle.wrapKey(
"pkcs8",
signingKey,
wrappingKey,
{ name: "AES-GCM", iv: iv }
);
问题在于:wrapKey()操作会丢失密钥的usages属性。当解包时:
const unwrappedKey = await crypto.subtle.unwrapKey(
"pkcs8",
wrappedKey,
unwrappingKey,
{ name: "AES-GCM", iv: iv },
{ name: "RSASSA-PKCS1-v1_5" },
true, // extractable
["sign"] // usages由调用者指定!
);
注意到了吗?unwrapKey()的usages参数是由调用者指定的,而不是从原始密钥恢复的。这意味着攻击者可以:
- 获取一个只能用于签名的密钥
- 将其包装
- 解包时指定额外的用途,如
["sign", "verify"]或["decrypt"]
对于某些算法,这可能允许攻击者扩大密钥的权限范围。
教训
这个漏洞的根源在于API设计时的假设失误:设计者假设调用unwrapKey()的代码是可信的。但在XSS攻击场景下,任何JavaScript代码都可能被替换。
INRIA论文的结论是:“The behavior of key wrapping and key usages in the API would seem to violate the expectations of most developers who want to use the API.“这是一个典型的"开发者的心智模型与实际行为不一致"的问题。
流式加密:十年的未解难题
Web Crypto API最受诟病的限制之一是缺乏流式加密支持。对于大文件加密,这意味着必须将整个文件读入内存,这在移动设备上可能触发OOM错误。
这个问题可以追溯到Web Crypto API的第一个草案。GitHub上的issue #73从2013年就开始讨论这个需求,但直到2025年仍然没有解决。
为什么这么难?
流式加密的困难不在于算法——AES-GCM和ChaCha20-Poly1305都支持流式处理——而在于安全模型。
W3C规范定义了一个严格的操作模型:每次加密操作都是原子的,输入和输出都是完整的ArrayBuffer。如果要支持流式加密,需要重新设计:
- 状态管理:加密状态(如计数器值)如何在多个JavaScript调用之间保持?
- 错误处理:流中途出错,部分加密的数据如何处理?
- 密钥生命周期:如果加密过程被中断,密钥应该保持可用还是销毁?
Chrome团队曾明确表示反对流式加密,理由是复杂度过高且安全风险难以控制。
当前的变通方案
开发者目前只能使用分块加密:
async function encryptLargeFile(file, key) {
const chunkSize = 1024 * 1024; // 1MB chunks
const encryptedChunks = [];
for (let offset = 0; offset < file.size; offset += chunkSize) {
const chunk = file.slice(offset, offset + chunkSize);
const buffer = await chunk.arrayBuffer();
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: iv },
key,
buffer
);
encryptedChunks.push({ iv, data: encrypted });
}
return encryptedChunks;
}
这种方案的问题是需要为每个块生成独立的IV,增加了复杂性和出错风险。
2025年,WinterTC(一个专注于Web互操作服务器运行时的技术委员会)提出了一个流式加密提案,但浏览器厂商的态度仍然谨慎。WebKit团队表示可能会优先支持流式哈希,而不是流式加密。
性能:原生vsJavaScript实现
Web Crypto API的另一个卖点是性能。WebKit团队的基准测试显示,Web Crypto API比纯JavaScript实现的加密库快2-15倍。
具体数据
在一项对比测试中,加密1MB数据:
| 实现方式 | AES-GCM耗时 |
|---|---|
| Web Crypto API | ~2ms |
| SJCL (JavaScript) | ~30ms |
| Forge (JavaScript) | ~45ms |
差异来自多个层面:
- 原生代码vsJIT编译:浏览器使用C/C++实现加密算法,编译为机器码执行
- 硬件加速:AES-NI指令集在支持AES硬件加速的CPU上可提供10倍以上的性能提升
- 内存管理:原生实现避免了JavaScript对象的创建和垃圾回收开销
WebAssembly的挑战
有人可能会问:WebAssembly能否提供类似性能?理论上可以,但实际上WebAssembly与Web Crypto API的定位不同:
- WebAssembly加密库仍然需要将密钥存储在JavaScript可访问的内存中
- Web Crypto API的密钥存储在浏览器管理的隔离空间中
安全性上的差异使得Web Crypto API在关键场景中仍然不可替代。
算法支持的演进
Web Crypto API支持的算法列表随时间演变,反映了密码学领域的发展。
被移除的算法
RSAES-PKCS1-v1_5曾是最广泛使用的RSA加密方案,但Web Crypto API Level 1规范将其标记为"deprecated”,最终在Level 2中完全移除。
原因是Manger在2001年发现的padding oracle攻击。攻击者可以通过观察解密操作的成功/失败响应,逐字节恢复明文。虽然可以通过严格的实现避免这个漏洞,但W3C选择了更安全的方案——只保留RSA-OAEP。
这个决定体现了Web Crypto API的一个设计原则:宁可限制功能,也不要给开发者留下安全陷阱。
新增的算法:Ed25519与X25519
Curve25519曲线的支持是近年来最重要的更新。2024年8月,Firefox 129率先支持Ed25519签名和X25519密钥交换。2025年,Chrome M137和Safari也加入了支持。
Igalia的Javier Fernandez在博客中详细描述了这个过程:“This is the last milestone of a three-year collaboration between Protocol Labs (who initiated the project), the IPFS Foundation, Open Impact Foundation, and WebTransitions.org.”
Ed25519的优势在于:
- 更短的密钥(32字节 vs RSA的2048位)
- 更快的签名和验证速度
- 无需担心随机数生成器质量(确定性签名)
对于IPFS、Solana等使用Ed25519作为身份标识的系统,浏览器原生支持意味着不再需要引入405KB的polyfill库(如@noble/ed25519),既减少bundle大小,又消除了供应链攻击面。
后量子密码学:下一个前沿
NIST在2024年发布了后量子密码学标准,其中ML-KEM(原名Kyber)作为密钥封装机制被标准化。Web Crypto API的演进草案已经包含了ML-KEM的支持计划。
WICG的"Modern Algorithms in the Web Cryptography API"草案定义了新的API:
// 提案中的ML-KEM API
const { sharedKey, ciphertext } = await crypto.subtle.encapsulateBits(
{ name: "ML-KEM-768" },
publicKey
);
const sharedKey = await crypto.subtle.decapsulateBits(
{ name: "ML-KEM-768" },
privateKey,
ciphertext
);
dchest.com的博主已经发布了mlkem-wasm库,提供与提案兼容的接口,方便开发者提前准备迁移。基准测试显示,在M1 MacBook Air上,ML-KEM-768的密钥生成只需0.04ms,封装/解封装约0.03-0.04ms。
但需要注意的是,ML-KEM仍然是一个相对较新的算法,安全社区对其研究深度远不及AES和椭圆曲线算法。大多数实现采用混合方案——同时使用ML-KEM和X25519,任何一方被攻破都不会导致整体安全失效。
开发者的常见误解
Web Crypto API的文档和教程不少,但开发者在使用时仍然容易陷入一些误区。
误解一:Web Crypto API可以防止所有XSS攻击
这是一个危险的误解。extractable: false只能防止密钥被导出,但攻击者仍然可以使用密钥执行签名、解密等操作。
正确的理解是:Web Crypto API限制了攻击者在获得代码执行能力后能做的事情,而不是阻止攻击者获得代码执行能力。应用层的安全措施(如CSP、输入验证、输出编码)仍然不可或缺。
误解二:浏览器存储的密钥是永久安全的
IndexedDB中的CryptoKey会在用户清除浏览器数据时丢失。没有备份机制的密钥管理方案会导致用户数据永久无法解密。
一个合理的方案是:使用用户密码派生主密钥,然后用主密钥包装其他密钥,将包装后的密钥存储在服务器。
误解三:所有浏览器都支持相同的算法
Web Crypto API规范并不要求浏览器实现所有算法。开发者应该使用特性检测:
// 检查算法支持(现代浏览器)
if (crypto.subtle.supports) {
const supported = crypto.subtle.supports('generateKey', { name: 'Ed25519' });
}
// 兼容方案:尝试生成并捕获错误
async function checkEd25519Support() {
try {
await crypto.subtle.generateKey({ name: "Ed25519" }, false, ["sign"]);
return true;
} catch {
return false;
}
}
误解四:ECDSA签名每次都相同
ECDSA签名需要一个随机数k。如果相同的k被重复使用,私钥可以从两个签名中恢复出来——这是PlayStation 3被破解的根本原因。
Web Crypto API的实现是安全的,每次签名都会生成新的随机数。但开发者需要知道:如果切换到其他实现(如WebAssembly库),必须确保其也正确处理随机数。Ed25519的设计优势在于其确定性签名——不依赖随机数生成器。
展望:Web Crypto API的未来
Web Crypto API Level 2草案正在制定中,包含多项改进:
- ChaCha20-Poly1305:在没有AES硬件加速的设备上性能更优
- SHA-3和cSHAKE:NIST推荐的哈希算法更新
- Argon2:密码派生函数,替代已过时的PBKDF2
- ML-KEM/ML-DSA:后量子密码学算法
但这些改进的实现进度取决于浏览器厂商的优先级。Chrome团队的态度尤为关键——它占据约65%的市场份额。
另一个值得关注的方向是Web Crypto API与Web Authentication API(WebAuthn)的集成。WebAuthn使用硬件安全模块(如YubiKey、TPM)存储密钥,提供了比软件存储更高的安全保证。两个API的融合可能会带来新的安全模型。
Web Crypto API的诞生,本质上是对JavaScript安全困境的一次回应。它没有试图修复JavaScript的缺陷——那是不可能的——而是在浏览器层面创建了一个隔离的安全边界。
这个设计选择是明智的,但也是有代价的。开发者需要理解这个边界在哪里、它能保护什么、不能保护什么。安全不是一个API能够提供的,而是一个需要贯穿设计、实现、运维全过程的系统工程。
当密钥逃离JavaScript,它获得了一层保护。但保护密钥只是安全的一部分。更重要的是保护那些依赖密钥的系统——这需要开发者的持续警惕和正确使用。
参考资料
-
W3C. Web Cryptography API Level 1. W3C Recommendation, January 2017. https://www.w3.org/TR/WebCryptoAPI/
-
Halpin, H. “Re-igniting the Crypto Wars on the Web”. 29th Chaos Communication Congress, 2012.
-
INRIA. “Security Analysis of the W3C Web Cryptography API”. HAL Archives, January 2017. https://inria.hal.science/hal-01426852/document
-
WebKit Blog. “Update on Web Cryptography”. July 2017. https://webkit.org/blog/7790/update-on-web-cryptography/
-
Igalia. “Ed25519 Support Lands in Chrome: What It Means for Developers”. August 2025. https://blogs.igalia.com/jfernandez/2025/08/25/ed25519-support-lands-in-chrome-what-it-means-for-developers-and-the-web/
-
WICG. “Modern Algorithms in the Web Cryptography API”. Draft Specification. https://wicg.github.io/webcrypto-modern-algos/
-
dchest.com. “ML-KEM in WebCrypto API”. August 2025. https://dchest.com/2025/08/09/mlkem-webcrypto/
-
inovex. “The Web Cryptography API: Do not Trust Anybody! [Part 1]”. September 2020. https://www.inovex.de/de/blog/web-cryptography-api-part-1/
-
MDN Web Docs. “Web Crypto API”. https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API
-
Ptacek, T. “JavaScript Cryptography Considered Harmful”. 2011.
-
NIST. “FIPS 203: Module-Lattice-Based Key-Encapsulation Mechanism Standard”. 2024.
-
WinterTC. “WebCrypto Streams Proposal”. https://github.com/wintercg/proposal-webcrypto-streams
-
IPFS Foundation. “Ed25519 Support in Chrome: Making the Web Faster and Safer”. August 2025.
-
Stanford Computer Security Lab. “Stanford JavaScript Crypto Library (SJCL)”. http://bitwiseshiftleft.github.io/sjcl/
-
NPM. “@noble/curves - Ed25519 implementation”. https://www.npmjs.com/package/@noble/curves