尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Web Crypto API 实战指南:现代前端加密技术核心原理与应用

Web Crypto API 实战指南:现代前端加密技术核心原理与应用 1. 项目概述为什么现代Web应用必须掌握加密技术如果你还在用md5或者sha1来“加密”用户密码或者觉得前端加密是多此一举那这篇文章就是为你准备的。随着Web应用变得越来越复杂从前端表单提交、本地数据存储到与后端API的安全通信甚至是在浏览器内直接处理敏感文件对加密技术的需求已经从后端专家的专属领域蔓延到了每一个前端和全栈开发者的日常工作里。我见过太多项目因为对加密的轻视或误用导致了数据泄露、中间人攻击甚至法律风险。现代JavaScript生态为我们提供了强大的武器Web Crypto API。这不是一个第三方库而是内置于现代浏览器中的原生标准API意味着它更安全、更高效并且无需额外的网络请求加载。它解决了我们在纯前端环境中实现可靠加密的痛点比如生成安全的密钥、进行高效的加解密操作、生成数字签名等。但它的文档相对晦涩概念繁杂直接上手容易踩坑。本文将带你绕过这些坑从核心概念到实战代码彻底搞懂如何在你的下一个项目中正确、安全地使用现代JavaScript加密技术。2. Web Crypto API 核心概念与架构解析在撸起袖子写代码之前我们必须先理解Web Crypto API的设计哲学和几个关键概念。这能帮你避免“代码能跑但安全漏洞百出”的尴尬局面。2.1 核心设计操作隔离与密钥安全Web Crypto API 最核心的设计原则是“密钥材料不出安全边界”。这是什么意思简单类比它就像一个高度戒备的银行金库浏览器内部的加密上下文。你可以把原材料原始数据送进去也可以把金库里的成品加密后的数据或签名拿出来但你永远无法直接触摸或看到金库里的“印钞机”和“金钥匙”密钥的原始字节。你只能通过一些“指令窗口”API方法来命令金库里的机器用特定的钥匙做特定的事。这种设计带来了两大好处安全性即使你的网页被注入了恶意脚本XSS攻击者也无法直接窃取存储在CryptoKey对象中的密钥原始数据。性能加解密运算通常在接近底层的、优化的环境中执行速度比纯JavaScript实现的库要快得多。2.2 关键对象与工作流整个API围绕几个核心对象运转Crypto 与 SubtleCryptowindow.crypto或crypto.subtle是你的主要入口。crypto对象提供一些随机数生成等基础功能而crypto.subtle则提供了所有“微妙”的、强大的加密操作如加解密、签名、密钥派生等。subtle属性名意在提醒开发者这些功能使用不当会带来微妙而危险的安全问题。CryptoKey这是密钥的容器对象是安全性的核心。一个CryptoKey对象包含了算法信息、用途usages如encrypt,decrypt,sign,verify等以及类型type如secret对称密钥、public/private非对称密钥对。你无法直接读取它的密钥数据。ArrayBuffer 与 TypedArrayWeb Crypto API 几乎只与二进制数据打交道输入输出通常是ArrayBuffer。这意味着你经常需要在字符串如用户输入的密码和ArrayBuffer之间进行转换这是一个常见的“坑点”。一个典型的加密工作流如下生成或导入密钥要么通过API生成一个新的CryptoKey要么将一个已有的密钥数据如从服务器接收的JWK格式密钥导入为一个CryptoKey对象。执行加密操作调用subtle.encrypt()、subtle.sign()等方法传入密钥、算法参数和待处理的数据ArrayBuffer。处理结果接收一个ArrayBuffer格式的结果你可能需要将其转换为Base64字符串以便网络传输或转换为十六进制字符串用于调试。注意crypto.subtle下的所有方法返回的都是Promise。这是因为它的一些操作如密钥生成可能是计算密集型的异步设计可以避免阻塞主线程。务必使用async/await或.then()来处理。3. 常见加密算法实战与选型指南理论说再多不如一行代码。下面我们针对几种最常见的场景看看如何用Web Crypto API实现并深入聊聊为什么选这个算法参数又该怎么设置。3.1 场景一对称加密AES-GCM—— 加密本地存储的数据假设你要在用户的localStorage里存一些敏感配置但不想明文存放。AES-GCMGalois/Counter Mode是目前推荐的对称加密模式因为它同时提供了保密性和完整性验证防止密文被篡改。// 工具函数字符串与ArrayBuffer互转 function strToAb(str) { return new TextEncoder().encode(str); } function abToStr(ab) { return new TextEncoder().decode(ab); } // 工具函数ArrayBuffer与Base64互转用于存储或传输 function abToBase64(ab) { return btoa(String.fromCharCode(...new Uint8Array(ab))); } function base64ToAb(base64) { return Uint8Array.from(atob(base64), c c.charCodeAt(0)).buffer; } async function encryptLocalData(secretData, password) { // 1. 从密码派生一个密钥 (使用 PBKDF2) const salt crypto.getRandomValues(new Uint8Array(16)); // 随机盐值必须保存 const baseKey await crypto.subtle.importKey( raw, strToAb(password), { name: PBKDF2 }, false, [deriveKey] ); const aesKey await crypto.subtle.deriveKey( { name: PBKDF2, salt: salt, iterations: 100000, // 迭代次数增加暴力破解成本 hash: SHA-256 }, baseKey, { name: AES-GCM, length: 256 }, // 生成一个256位的AES密钥 false, // 是否可导出密钥通常设为false更安全 [encrypt, decrypt] ); // 2. 执行加密 const iv crypto.getRandomValues(new Uint8Array(12)); // GCM推荐12字节IV const encryptedContent await crypto.subtle.encrypt( { name: AES-GCM, iv: iv }, aesKey, strToAb(secretData) ); // 3. 打包结果需要存储盐、IV和密文 const result { salt: abToBase64(salt.buffer), iv: abToBase64(iv.buffer), ciphertext: abToBase64(encryptedContent) }; return JSON.stringify(result); } async function decryptLocalData(encryptedPackage, password) { const packageObj JSON.parse(encryptedPackage); const salt base64ToAb(packageObj.salt); const iv base64ToAb(packageObj.iv); const ciphertext base64ToAb(packageObj.ciphertext); // 重新派生密钥使用相同的盐和参数 const baseKey await crypto.subtle.importKey(raw, strToAb(password), { name: PBKDF2 }, false, [deriveKey]); const aesKey await crypto.subtle.deriveKey( { name: PBKDF2, salt: salt, iterations: 100000, hash: SHA-256 }, baseKey, { name: AES-GCM, length: 256 }, false, [decrypt] // 这里用途是解密 ); // 执行解密 try { const decrypted await crypto.subtle.decrypt( { name: AES-GCM, iv: iv }, aesKey, ciphertext ); return abToStr(decrypted); } catch (e) { // 如果密码错误、数据被篡改decrypt会抛出异常 console.error(解密失败:, e); return null; } } // 使用示例 (async () { const mySecret 这是我的机密配置信息; const userPassword StrongPassword123!; const encrypted await encryptLocalData(mySecret, userPassword); console.log(加密后存储的内容:, encrypted); const decrypted await decryptLocalData(encrypted, userPassword); console.log(解密后的内容:, decrypted); // 应输出原始信息 })();实操心得与参数解读为什么用PBKDF2用户输入的密码通常不够随机熵低。PBKDF2基于密码的密钥派生函数2通过加入随机salt和多次iterations哈希计算能将一个弱密码派生出一个强壮的、适合加密的密钥。salt不需要保密但必须唯一通常随机生成并与密文一起存储。iterations设多少10万次是一个2020年后的安全基准。这个值需要在安全迭代次数多破解慢和用户体验计算耗时之间权衡。可以前端动态测试设备性能选择一个在1秒内完成的值。为什么是AES-GCM相比旧的CBC模式GCM是认证加密模式。它不仅能加密还会生成一个认证标签TagAPI内自动处理解密时会验证密文和IV是否被篡改。如果被改decrypt方法会直接抛出异常而不是输出乱码。永远不要使用ECB模式它是不安全的。IV初始化向量的重要性即使相同的密钥和明文每次加密也必须使用不同的随机IV以防止模式分析攻击。对于GCM12字节的随机IV是标准做法。3.2 场景二非对称加密与签名RSA-OAEP / ECDSA—— API请求验签当你的前端需要向后端发送不可抵赖的请求或验证后端返回的数据时就需要非对称加密。常见组合是RSA-OAEP用于加密小段数据如一个对称密钥ECDSA用于数字签名。// 生成RSA密钥对用于加密传输一个随机的对称密钥 async function generateRsaKeyPair() { return await crypto.subtle.generateKey( { name: RSA-OAEP, modulusLength: 2048, // 密钥长度2048是当前最低安全要求考虑升级到3072 publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537标准值 hash: SHA-256 }, true, // 是否可导出这里设为true方便演示导出公钥发给后端 [encrypt, decrypt] // 密钥用途 ); } // 生成ECDSA密钥对用于数字签名 async function generateEcdsaKeyPair() { return await crypto.subtle.generateKey( { name: ECDSA, namedCurve: P-256 // 使用NIST P-256曲线广泛支持且安全 }, true, [sign, verify] ); } // 模拟一个完整的流程前端签名请求后端验证后端加密响应前端解密 async function simulateApiInteraction() { // 前端生成签名密钥对通常长期保存 const { privateKey: clientSignKey, publicKey: clientPublicKey } await generateEcdsaKeyPair(); // 前端生成加密密钥对或由后端下发公钥 const { privateKey: clientRsaPrivateKey, publicKey: clientRsaPublicKey } await generateRsaKeyPair(); // --- 前端构造并签名请求 --- const requestBody { userId: 123, action: getBalance }; const requestBodyStr JSON.stringify(requestBody); // 1. 对请求体进行签名 const signature await crypto.subtle.sign( { name: ECDSA, hash: { name: SHA-256 } // 指定先对数据做SHA-256哈希再对哈希值签名 }, clientSignKey, // 用私钥签名 strToAb(requestBodyStr) ); // 将请求体、签名和公钥用于验证发送给后端 const requestToServer { body: requestBodyStr, signature: abToBase64(signature), publicKeyForVerify: await crypto.subtle.exportKey(spki, clientPublicKey) // 导出公钥格式 }; console.log(前端发送的请求包:, requestToServer); // --- 模拟后端验证签名 --- // 后端导入公钥 const importedPublicKey await crypto.subtle.importKey( spki, requestToServer.publicKeyForVerify, { name: ECDSA, namedCurve: P-256 }, true, [verify] ); // 验证签名 const isSignatureValid await crypto.subtle.verify( { name: ECDSA, hash: { name: SHA-256 } }, importedPublicKey, base64ToAb(requestToServer.signature), strToAb(requestToServer.body) ); console.log(后端验证签名结果:, isSignatureValid ? ✅ 验签成功 : ❌ 验签失败); // --- 模拟后端生成响应并用RSA加密 --- const sensitiveResponse JSON.stringify({ balance: 10000, currency: USD }); // 后端生成一个随机的对称密钥如AES密钥用于加密实际响应数据 const aesKeyForResponse await crypto.subtle.generateKey( { name: AES-GCM, length: 256 }, true, [encrypt, decrypt] ); const ivForResponse crypto.getRandomValues(new Uint8Array(12)); const encryptedResponse await crypto.subtle.encrypt( { name: AES-GCM, iv: ivForResponse }, aesKeyForResponse, strToAb(sensitiveResponse) ); // 后端用前端的RSA公钥加密上一步生成的对称密钥 const encryptedAesKey await crypto.subtle.encrypt( { name: RSA-OAEP }, clientRsaPublicKey, // 使用前端的公钥加密 await crypto.subtle.exportKey(raw, aesKeyForResponse) // 导出对称密钥的原始数据 ); // 后端将加密后的对称密钥、IV和密文一起发给前端 const responseToClient { encryptedKey: abToBase64(encryptedAesKey), iv: abToBase64(ivForResponse.buffer), ciphertext: abToBase64(encryptedResponse) }; console.log(后端发送的加密响应包:, responseToClient); // --- 模拟前端解密响应 --- // 1. 前端用自己的RSA私钥解密出对称密钥 const decryptedAesKeyData await crypto.subtle.decrypt( { name: RSA-OAEP }, clientRsaPrivateKey, base64ToAb(responseToClient.encryptedKey) ); // 2. 将解密出的密钥数据导入为CryptoKey对象 const importedAesKey await crypto.subtle.importKey( raw, decryptedAesKeyData, { name: AES-GCM, length: 256 }, true, [decrypt] ); // 3. 用对称密钥解密实际响应数据 const decryptedResponseData await crypto.subtle.decrypt( { name: AES-GCM, iv: base64ToAb(responseToClient.iv) }, importedAesKey, base64ToAb(responseToClient.ciphertext) ); const finalResponse JSON.parse(abToStr(decryptedResponseData)); console.log(前端解密后的响应:, finalResponse); } // 运行模拟 simulateApiInteraction().catch(console.error);算法选型深度解析RSA-OAEP vs RSA-PKCS1-v1_5务必选择OAEP最优非对称加密填充。PKCS1-v1_5是旧标准存在潜在的攻击风险。OAEP在安全性上更优是现代应用的标准选择。为什么用ECDSA签名而不是RSA签名在相同的安全强度下ECC椭圆曲线密码学的密钥长度比RSA短得多例如256位ECC ≈ 3072位RSA。这意味着签名更短、生成和验证速度更快、传输开销更小。对于移动端或高频API场景ECDSA优势明显。P-256曲线是行业广泛接受的标准。混合加密系统注意上述模拟中的模式。直接用RSA加密大量数据效率极低。因此实际模式是用RSA加密一个随机生成的对称密钥如AES密钥再用这个对称密钥去加密实际数据。这就是经典的“混合加密”系统兼顾了非对称加密的密钥分发优势和对称加密的效率优势。3.3 场景三哈希与消息认证码SHA-256, HMAC—— 数据完整性校验哈希用于确保数据完整性是否被修改HMAC则在哈希基础上加入了密钥用于在通信双方共享密钥的场景下同时验证完整性和真实性。// 计算数据的SHA-256哈希值不可逆 async function computeSha256(data) { const hashBuffer await crypto.subtle.digest(SHA-256, strToAb(data)); const hashArray Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b b.toString(16).padStart(2, 0)).join(); // 转为十六进制字符串 } // 计算HMAC基于密钥的哈希 async function computeHmac(key, data) { // 首先需要一个密钥。可以从固定字符串导入或随机生成。 const hmacKey await crypto.subtle.importKey( raw, strToAb(key), // 密钥字符串 { name: HMAC, hash: { name: SHA-256 } }, false, // 通常不允许导出HMAC密钥 [sign, verify] ); const signature await crypto.subtle.sign( HMAC, hmacKey, strToAb(data) ); // 同样转换为十六进制 return Array.from(new Uint8Array(signature)).map(b b.toString(16).padStart(2, 0)).join(); } // 使用示例验证文件分块上传的完整性 async function verifyFileChunk(chunkData, expectedHash, secretKey) { const computedHash await computeSha256(chunkData); if (computedHash ! expectedHash) { throw new Error(文件块哈希校验失败计算值: ${computedHash}, 期望值: ${expectedHash}); } // 如果还需要验证这个哈希值确实是服务器发送的而非被中间人替换可以用HMAC const computedHmac await computeHmac(secretKey, chunkData expectedHash); // 将数据和哈希拼接后计算HMAC // 服务器会发送一个对应的HMAC值前端进行比对 // if (computedHmac ! serverSentHmac) { ... } console.log(块哈希校验通过: ${computedHash}); } // 模拟 (async () { const myData 这是一个非常重要的文件块数据; const hash await computeSha256(myData); console.log(SHA-256哈希:, hash); const secret MySharedSecret; const hmac await computeHmac(secret, myData); console.log(HMAC:, hmac); // 模拟校验 await verifyFileChunk(myData, hash, secret); })();注意事项哈希不是加密哈希是单向的无法从哈希值还原原始数据。它用于指纹、校验和、承诺机制等。SHA-1已破MD5已亡绝对不要在新的安全相关场景中使用MD5或SHA-1。它们已被证明存在碰撞漏洞。SHA-256是目前安全应用的最低标准SHA-384和SHA-512则更安全。HMAC的密钥管理HMAC的安全性完全依赖于密钥的保密性。这个密钥需要在通信双方安全地共享且不能使用用户密码等低熵数据直接作为密钥。4. 密钥的生命周期管理实战生成和使用了密钥如何管理它们这是Web Crypto API应用中最容易被忽视也最关键的环节。4.1 密钥的生成、导入与导出生成如上文所示使用crypto.subtle.generateKey。导入当你从服务器收到一个密钥例如JWK格式的公钥时使用crypto.subtle.importKey。你必须知道密钥的确切格式raw,spki,pkcs8,jwk和算法参数。导出使用crypto.subtle.exportKey。谨慎导出私钥或对称密钥只有在确有必要例如备份到安全的密钥管理系统时才这样做且要确保导出过程的安全如在安全的上下文中。// 示例导出和导入JWK格式的ECDSA公钥 async function handleJwkKey() { const keyPair await generateEcdsaKeyPair(); // 导出公钥为JWK格式一种JSON表示法易于网络传输 const publicKeyJwk await crypto.subtle.exportKey(jwk, keyPair.publicKey); console.log(公钥JWK:, JSON.stringify(publicKeyJwk, null, 2)); // 从JWK格式导入公钥 const importedPublicKey await crypto.subtle.importKey( jwk, publicKeyJwk, { name: ECDSA, namedCurve: P-256 }, true, // 可导出根据需要 [verify] // 指定用途 ); console.log(导入的密钥用途:, importedPublicKey.usages); }4.2 密钥的存储策略黄金法则永远不要在客户端持久化存储私钥或高敏感度的对称密钥。短期会话密钥对于一次会话有效的密钥如用于加密本次上传文件的随机AES密钥可以存放在内存JavaScript变量中页面关闭即消失。长期密钥非对称密钥对的私钥考虑使用浏览器的window.crypto.subtle的wrapKey/unwrapKey功能用用户密码派生出的密钥来加密存储私钥。将加密后的私钥数据存到localStorage或IndexedDB。这样只有知道密码的用户才能解锁和使用私钥。这就是“基于密码的密钥封装”。对称密钥同上用用户密码封装后存储。或者根本不在客户端存储每次需要时从服务器获取服务器用该用户的公钥加密后下发。公钥可以安全地存储在任何地方甚至可以硬编码在前端代码里。4.3 密钥的封装与解封示例这是一个进阶但非常重要的模式用于安全地存储密钥。async function wrapKeyForStorage(masterKey, keyToWrap) { // masterKey: 一个用于加密其他密钥的“主密钥”通常从用户密码派生。 // keyToWrap: 需要被安全存储的密钥如RSA私钥。 const iv crypto.getRandomValues(new Uint8Array(12)); // 使用AES-GCM模式封装加密目标密钥 const wrappedKey await crypto.subtle.wrapKey( pkcs8, // 导出被封装密钥的格式 keyToWrap, masterKey, { name: AES-GCM, iv: iv } ); return { wrappedKeyData: abToBase64(wrappedKey), iv: abToBase64(iv.buffer) }; } async function unwrapKeyFromStorage(wrappedData, masterKey) { const { wrappedKeyData, iv } wrappedData; // 解封解密出原始密钥 return await crypto.subtle.unwrapKey( pkcs8, // 被封装密钥的格式 base64ToAb(wrappedKeyData), masterKey, { name: AES-GCM, iv: base64ToAb(iv) }, { name: RSA-OAEP, hash: SHA-256 }, // 被封装密钥的算法 true, // 是否可导出 [decrypt] // 被封装密钥的用途 ); } // 注意masterKey的生成和安全管理是另一个关键问题通常通过PBKDF2从用户密码派生。5. 浏览器兼容性、性能与安全陷阱5.1 兼容性检查与降级方案Web Crypto API 在现代浏览器中支持良好但一些老旧浏览器或特殊环境如某些国产浏览器可能不支持或不支持某些算法。// 特性检测 if (!window.crypto || !window.crypto.subtle) { console.error(此浏览器不支持Web Crypto API。); // 降级方案加载一个纯JavaScript的加密库如Stanford的jsrsasign或asmCrypto。 // 但请注意JS库性能更差且密钥材料可能暴露在JS环境中安全性降低。 } // 检测特定算法 async function checkAlgorithmSupport() { try { await crypto.subtle.generateKey({ name: AES-GCM, length: 256 }, false, [encrypt]); console.log(✅ 支持AES-GCM); } catch (e) { console.log(❌ 不支持AES-GCM); } }实操心得对于关键业务务必在应用初始化时进行算法支持检测并提供明确的用户提示或自动降级方案。永远不要假设API一定可用。5.2 性能考量密钥生成RSA 2048位以上密钥生成较慢可能几百毫秒到几秒应在后台线程Web Worker中进行避免阻塞UI。ECDSA密钥生成快得多。加解密操作对称加密AES极快。非对称加密RSA加密/解密尤其是解密较慢避免用它加密大量数据。PBKDF2迭代高迭代次数的PBKDF2是故意设计成慢的以抵御暴力破解。在前端执行时要考虑用户设备性能可能需要进行自适应调整例如在用户登录时测量一次派生耗时并据此调整迭代次数。5.3 常见安全陷阱与避坑指南IV/Nonce复用这是对称加密如AES-GCM中最致命的错误之一。同一个密钥下绝对不要使用相同的IV加密两条不同的消息。否则会严重破坏安全性。务必使用密码学安全的随机数生成器crypto.getRandomValues来生成IV。使用不安全的算法或参数避免使用DES、3DES、RC4、MD5、SHA-1、RSA with PKCS#1 v1.5 padding (用于加密)、ECB模式。坚持使用本文推荐的算法和参数。在客户端存储或处理真正的秘密前端代码对用户是透明的。任何嵌入在JS中的密钥、密码最终都可能被获取。前端加密的主要目的是在传输过程中或静态存储时提供保护防止特定类型的攻击如窃听网络流量、盗取本地存储而不是防止一个控制了用户浏览器的攻击者。真正的秘密如服务端私钥、数据库主密钥必须留在后端。错误处理信息泄露加解密失败时API会抛出异常。不要将详细的异常信息如“解密失败标签不匹配”直接展示给用户这可能会帮助攻击者。记录到内部日志给用户一个通用的错误提示即可。时间侧信道攻击虽然Web Crypto API的实现本身会努力避免基于时间的攻击但你的应用逻辑也可能引入风险。例如比较HMAC或验证签名时使用字符串直接比较会在第一个不匹配的字节就返回false攻击者可以通过测量响应时间差异来猜测正确的值。应使用恒定时间比较函数。不过在浏览器JavaScript中实现真正的恒定时间比较非常困难这通常是后端需要更关注的问题。6. 实战集成构建一个前端加密上传组件让我们综合运用以上知识构建一个模拟的“前端加密文件上传”组件核心逻辑。该组件会在文件上传前在浏览器内用随机生成的AES密钥加密文件再将加密后的文件和用服务器公钥加密的AES密钥一起上传。class SecureFileUploader { constructor(serverPublicKeySpki) { // 假设已从服务器获取RSA公钥(SPKI格式) this.serverPublicKey null; this.importServerPublicKey(serverPublicKeySpki); } async importServerPublicKey(spkiData) { // 导入服务器公钥用于加密对称密钥 this.serverPublicKey await crypto.subtle.importKey( spki, spkiData, { name: RSA-OAEP, hash: SHA-256 }, true, [encrypt] ); } async encryptFile(file) { // 1. 为本次上传生成一个随机的AES密钥和IV const aesKey await crypto.subtle.generateKey( { name: AES-GCM, length: 256 }, true, [encrypt] ); const iv crypto.getRandomValues(new Uint8Array(12)); // 2. 读取文件内容 const fileBuffer await file.arrayBuffer(); // 3. 加密文件内容 const encryptedFileBuffer await crypto.subtle.encrypt( { name: AES-GCM, iv: iv }, aesKey, fileBuffer ); // 4. 导出AES密钥的原始数据并用服务器公钥加密它 const rawAesKey await crypto.subtle.exportKey(raw, aesKey); const encryptedAesKey await crypto.subtle.encrypt( { name: RSA-OAEP }, this.serverPublicKey, rawAesKey ); // 5. 计算加密后文件的哈希用于完整性校验可选 const fileHashBuffer await crypto.subtle.digest(SHA-256, encryptedFileBuffer); const fileHashHex Array.from(new Uint8Array(fileHashBuffer)).map(b b.toString(16).padStart(2, 0)).join(); return { encryptedFile: new Blob([encryptedFileBuffer], { type: application/octet-stream }), encryptedKey: abToBase64(encryptedAesKey), iv: abToBase64(iv.buffer), fileHash: fileHashHex, originalName: file.name, originalType: file.type }; } async upload(encryptedPackage) { // 模拟上传过程 const formData new FormData(); formData.append(encryptedFile, encryptedPackage.encryptedFile, encrypted_${encryptedPackage.originalName}); formData.append(encryptedKey, encryptedPackage.encryptedKey); formData.append(iv, encryptedPackage.iv); formData.append(fileHash, encryptedPackage.fileHash); formData.append(fileName, encryptedPackage.originalName); console.log(开始上传加密包..., encryptedPackage); // 这里实际使用 fetch API 上传 formData // const response await fetch(/api/secure-upload, { method: POST, body: formData }); // return response.json(); return { success: true, message: 上传模拟完成 }; } } // 使用示例 (async () { // 模拟一个服务器公钥实际应从后端API动态获取 const { publicKey: simulatedServerPublicKey } await crypto.subtle.generateKey( { name: RSA-OAEP, modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: SHA-256 }, true, [encrypt] ); const exportedServerPublicKey await crypto.subtle.exportKey(spki, simulatedServerPublicKey); const uploader new SecureFileUploader(exportedServerPublicKey); // 假设有一个文件输入框 input typefile idfileInput const fileInput document.getElementById(fileInput); fileInput.addEventListener(change, async (e) { const file e.target.files[0]; if (!file) return; try { console.log(开始加密文件: ${file.name}); const encryptedPackage await uploader.encryptFile(file); console.log(文件加密完成开始上传...); const result await uploader.upload(encryptedPackage); console.log(上传结果:, result); } catch (error) { console.error(处理文件时发生错误:, error); } }); })();这个组件实现了“端到端”加密上传文件在离开浏览器前已被加密服务器持有对应的私钥才能解密。即使传输过程被拦截或服务器存储被入侵攻击者没有服务器私钥也无法解密文件内容。服务器收到后用自己的私钥解密出AES密钥再用AES密钥解密文件。最后再分享一个关键技巧在处理大文件时上述一次性读取整个文件到内存file.arrayBuffer()可能会造成内存压力。更优的做法是使用流式加密。虽然Web Crypto API原生不支持流但你可以将文件分块例如每1MB一块为每一块使用相同的密钥但不同的IV例如将初始IV与块索引进行组合进行加密。这需要更精细的设计来确保每个IV的唯一性并可能需要将分块信息一并上传由服务器端按顺序解密和重组。这是实现生产级大文件前端加密的必要步骤。
返回列表