1. 项目概述:为什么你需要一个JavaScript加密库?
如果你正在开发一个Web应用,无论是处理用户密码、保护API通信,还是在前端对敏感数据进行预处理,加密都是一个绕不开的话题。你可能遇到过这样的场景:用户注册时,密码明文发送到后端让你心惊胆战;或者,你想在本地存储一些用户偏好设置,但又不想让这些数据在浏览器里“裸奔”。这时候,一个可靠、易用的前端加密库就成了必需品。
CryptoJS正是为此而生。它是一个纯JavaScript实现的加密算法库,支持AES、DES、SHA-256等多种标准算法。它的核心价值在于,让你能在浏览器环境中,不依赖后端,就完成可靠的加密、解密、哈希计算等操作。这对于构建更安全、更注重隐私的现代Web应用至关重要。想象一下,用户在表单里输入密码,你可以在点击“提交”按钮的瞬间,用CryptoJS的SHA-256将其哈希化,这样传输到服务器的就不再是原始密码,大大降低了中间人攻击的风险。或者,你想做一个离线可用的笔记应用,用AES加密本地存储的笔记内容,只有输入正确密码的用户才能解密查看,这为用户数据增加了一层强有力的客户端保护。
然而,网络上的教程质量参差不齐。很多文章只给出一段代码,却不解释密钥从哪里来、IV(初始化向量)是什么、为什么加密后的结果是一串奇怪的字符串。更常见的是,开发者直接拷贝代码后,在控制台遇到“Malformed UTF-8 data”或“Invalid key length”这样的错误弹窗,然后陷入无尽的调试。这正是本指南要解决的问题——我们不只给你代码,更要带你理解CryptoJS的每一个核心概念、参数和最佳实践,让你能真正“掌握”而不仅仅是“使用”它。
2. 核心概念与算法选型:在开始写代码之前
在动手引入CryptoJS之前,我们必须先理清几个基础但至关重要的概念。加密领域术语繁多,理解它们是你避免后续各种“javascript error occurred”的关键。
2.1 对称加密 vs. 非对称加密
这是两种根本不同的加密范式。对称加密,如AES和DES,加密和解密使用同一把密钥。它的优点是速度快,适合加密大量数据。但密钥的分发和保管是个难题:你怎么安全地把密钥告诉通信的另一方?非对称加密,如RSA,使用公钥和私钥一对密钥。公钥可以公开,用于加密;私钥自己保管,用于解密。它解决了密钥分发问题,但速度慢得多,通常只用于加密小数据(如一个会话密钥)或数字签名。
在前端场景中,CryptoJS主要处理对称加密。因为非对称加密计算量大,且私钥在前端(一个不可信的环境)保管是极不安全的。所以,常见的模式是:后端生成一个随机的AES密钥,用RSA公钥加密后传给前端;前端用RSA私钥(如果安全地嵌入的话,通常不推荐)解密得到AES密钥,再用它加解密数据。更安全的做法是,所有非对称操作都在后端完成,前端只负责用后端下发的对称密钥进行加解密。
2.2 理解AES:块加密的模式与填充
AES(高级加密标准)是当前最主流的对称加密算法。CryptoJS对其有很好的支持。但直接使用CryptoJS.AES.encrypt你会遇到两个必须理解的参数:模式(Mode)和填充(Padding)。
- 模式:AES是块加密算法,一次处理固定长度(128位,即16字节)的数据。如果你的明文不是16字节的整数倍怎么办?模式定义了如何将长数据切割成块,以及块之间如何关联。最常见的是CBC模式,它需要一个初始化向量(IV)。IV是一个随机数,用于确保即使加密相同的明文,每次产生的密文也不同,这极大地增强了安全性。绝对不要重复使用同一个IV和密钥的组合。
- 填充:当最后一个数据块不足16字节时,需要填充至满块。
PKCS#7(在CryptoJS中叫Pkcs7)是最常用的填充方式。
如果你不指定,CryptoJS默认使用CBC模式和Pkcs7填充。但你必须显式地、安全地生成IV,并把它和密文一起存储或传输(IV不是秘密,可以公开)。
2.3 编码的迷宫:WordArray、Base64与Hex
这是新手最容易掉进去的坑。CryptoJS内部使用一种叫WordArray的对象来处理二进制数据。但加密后的结果,或者你提供的密钥、IV,通常需要以字符串形式展示或传输。
CryptoJS.enc.Hex: 十六进制编码,字符集为0-9, a-f。例如,4a代表一个字节。CryptoJS.enc.Base64: Base64编码,这是网络上传输二进制数据最常用的方式,字符集包含A-Z, a-z, 0-9, +, /,末尾可能有=。CryptoJS.enc.Utf8: UTF-8字符串编码。当你有一个普通字符串(如密码“mySecret”)时,需要用它转换成CryptoJS能处理的格式。
一个典型的错误是:从输入框获取一个字符串密钥,直接传给encrypt方法。CryptoJS期望密钥是一个WordArray。所以你必须先进行编码转换:CryptoJS.enc.Utf8.parse(“mySecretKey”)。同样,加密输出的结果是一个CryptoJS.lib.CipherParams对象,你需要调用.toString()并指定编码(如CryptoJS.enc.Base64)才能得到常见的字符串。
注意:密钥和IV的生成至关重要。永远不要使用像“password123”这样简单的字符串作为密钥。应该使用强随机数生成器生成足够长度(AES-256需要32字节)的密钥。在前端,可以使用
CryptoJS.lib.WordArray.random(32)来生成一个安全的随机密钥(字节数组)。
3. 环境搭建与基础使用:从引入到第一个加密函数
理解了理论,我们开始动手。首先是如何获取和使用CryptoJS。
3.1 多种引入方式
CryptoJS的官方维护版本可以通过CDN、NPM或直接下载源码引入。
- CDN(最快上手): 在HTML文件中直接引入。这对于快速原型或学习是最方便的。
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"></script> <!-- 或者按需引入核心和AES模块 --> <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/core.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/aes.min.js"></script> - NPM/Yarn(现代项目推荐): 在Node.js或使用Webpack等构建工具的前端项目中,这是标准方式。
然后按需导入:npm install crypto-js # 或 yarn add crypto-js// 导入整个库 import CryptoJS from 'crypto-js'; // 或按需导入以减少打包体积 import AES from 'crypto-js/aes'; import encUTF8 from 'crypto-js/enc-utf8'; import encBase64 from 'crypto-js/enc-base64';
3.2 你的第一个AES加密解密示例
让我们从一个完整的、注释详尽的例子开始,它涵盖了加密、解密以及正确的参数处理。
// 示例:使用AES-CBC模式加密和解密一段文本 // 1. 定义明文、密钥和IV(初始化向量) const plainText = “这是一段需要加密的敏感信息”; // 密钥:必须是16、24或32字节(对应AES-128, AES-192, AES-256)。这里使用一个UTF-8字符串,但实际应用应用随机字节。 const secretKey = “My32CharLongSuperSecretKey123!”; // 32个字符,UTF-8编码后正好32字节(AES-256) const iv = CryptoJS.lib.WordArray.random(16); // 生成一个16字节的随机IV // 2. 将字符串密钥和IV转换为CryptoJS内部格式(WordArray) const key = CryptoJS.enc.Utf8.parse(secretKey); // iv已经是WordArray,无需再次parse // 3. 执行加密 // 选项对象指定模式为CBC,填充为Pkcs7 const encrypted = CryptoJS.AES.encrypt(plainText, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); // 4. 获取密文字符串(通常用Base64传输) const cipherText = encrypted.toString(); console.log(“加密后的Base64密文:”, cipherText); console.log(“使用的IV (Hex):”, iv.toString(CryptoJS.enc.Hex)); // ———————————————————————— // 解密过程 // ———————————————————————— // 5. 假设我们收到了密文和IV,开始解密 // 注意:解密时需要提供与加密时完全相同的key和iv const decrypted = CryptoJS.AES.decrypt(cipherText, key, { iv: iv, // 这里必须使用加密时的同一个iv对象,或者用Hex/Base64字符串重新parse出来 mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); // 6. 将解密后的WordArray转回UTF-8字符串 const decryptedText = decrypted.toString(CryptoJS.enc.Utf8); console.log(“解密后的明文:”, decryptedText); // 应该与最初的plainText一致这个例子揭示了几个关键点:
- 密钥管理:示例中密钥是硬编码的字符串,这极不安全。生产环境中,密钥应由安全的密钥管理系统生成、分发和轮换,或者由后端通过安全通道(如HTTPS)动态提供给前端。
- IV的传递:IV不需要保密,但必须唯一且不可预测。通常,将IV(Hex或Base64格式)和密文拼接在一起传输,解密时再分开。例如:
${ivHex}:${cipherTextBase64}。 - 错误处理:如果密钥、IV或密文在传输过程中被篡改,
decrypt方法不会抛出异常,但decrypted.toString()的结果会是一串乱码。因此,对于重要数据,建议增加消息认证码(MAC)来验证完整性,或者使用AEAD(认证加密)模式如GCM(CryptoJS也支持)。
4. 深入核心算法:哈希、HMAC与其它加密函数
除了AES,CryptoJS还提供了丰富的密码学原语。理解它们的用途是构建安全应用的基础。
4.1 哈希函数:SHA家族与消息摘要
哈希函数(如SHA-256)是单向的。它把任意长度的数据映射成固定长度的“指纹”(摘要)。核心特性是:不可逆(无法从摘要还原原文)、抗碰撞(极难找到两个不同原文产生相同摘要)。它常用于:
- 密码存储:不在数据库存明文密码,而是存其哈希值(必须加盐!)。
- 数据完整性校验:计算文件的哈希值,下载后重新计算比对,确保文件未被篡改。
- 生成唯一标识。
// 计算字符串的SHA-256哈希值 const message = “Hello, CryptoJS!”; const hash = CryptoJS.SHA256(message); console.log(“SHA-256 Hex:”, hash.toString(CryptoJS.enc.Hex)); // 输出: 8c1c...(64位十六进制字符串) // 重要:哈希是单向的。以下操作是无效的: // const original = hash.reverseOperation(); // 不存在这样的方法!实操心得:关于“加盐”:直接对密码进行哈希(如
SHA256(password))是不安全的,因为攻击者可以使用彩虹表进行反向查找。必须为每个密码添加一个唯一的随机字符串(盐),然后对盐+密码的组合进行哈希。即使两个用户密码相同,由于盐不同,哈希值也完全不同。CryptoJS本身不提供自动加盐,你需要自己生成并存储盐。
4.2 HMAC:带密钥的哈希
HMAC可以看作“带密码的哈希”。它需要一个密钥,只有拥有相同密钥的双方才能计算出相同的消息认证码。这用于验证消息的完整性和真实性,确保消息来自合法的发送方且未被篡改。
const message = “重要的订单指令: Pay 100$ to Alice”; const secretKey = “shared-secret-key”; // 通信双方共享的密钥 const hmac = CryptoJS.HmacSHA256(message, secretKey); console.log(“HMAC-SHA256:”, hmac.toString(CryptoJS.enc.Hex)); // 验证方收到消息和HMAC后,用同样的密钥计算HMAC,比对是否一致。 const receivedMessage = “重要的订单指令: Pay 100$ to Alice”; const receivedHmac = “收到的HMAC字符串”; const calculatedHmac = CryptoJS.HmacSHA256(receivedMessage, secretKey).toString(CryptoJS.enc.Hex); if (calculatedHmac === receivedHmac) { console.log(“消息验证通过!”); } else { console.log(“警告!消息可能被篡改或来源不可信。”); }4.3 其他对称加密算法
- DES / 3DES: 较老的算法,密钥强度较弱(DES仅56位),已不被推荐用于新系统。CryptoJS支持它们主要是为了兼容旧协议。
- Rabbit、RC4: 流加密算法。RC4已被发现存在严重弱点,应避免使用。Rabbit相对较新且快速,但在需要高安全性的场合,AES仍是黄金标准。
算法选型建议:对于新项目,无脑选择AES-256(CBC或GCM模式)进行对称加密,选择SHA-256进行哈希,选择HMAC-SHA256进行消息认证。这是目前业界公认安全且广泛支持的组合。
5. 实战应用场景与完整代码剖析
掌握了基础,我们来看几个真实的前端开发场景,把知识串联起来。
5.1 场景一:前端密码传输哈希
目标:用户登录时,避免明文密码在网络上传输。 方案:前端对密码进行哈希(加盐),将哈希值传给后端。后端用同样的盐计算哈希并与数据库存储的哈希比对。
前端代码:
// 假设从后端接口获取该用户对应的盐(应在用户注册时生成并存入数据库) async function getSalt(username) { const response = await fetch(`/api/user/${username}/salt`); const data = await response.json(); return data.salt; // 一个Base64或Hex编码的随机字符串 } async function handleLogin(username, plainPassword) { // 1. 获取盐 const salt = await getSalt(username); const saltWordArray = CryptoJS.enc.Hex.parse(salt); // 假设盐是Hex格式 // 2. 密码加盐并哈希 (例如使用PBKDF2进行密钥派生,比简单哈希更安全) // 这里演示更安全的PBKDF2,它通过多次哈希迭代来增加破解难度 const keySize = 256 / 32; // 派生出的密钥长度(字) const iterations = 10000; // 迭代次数,越高越安全但也越慢 const hashedPassword = CryptoJS.PBKDF2(plainPassword, saltWordArray, { keySize: keySize, iterations: iterations, hasher: CryptoJS.algo.SHA256 }); // 3. 将派生出的密钥哈希值发送到后端 const passwordHashToSend = hashedPassword.toString(CryptoJS.enc.Hex); const loginResponse = await fetch(‘/api/login’, { method: ‘POST’, headers: { ‘Content-Type’: ‘application/json’ }, body: JSON.stringify({ username, passwordHash: passwordHashToSend }) }); // ... 处理响应 }注意:这只是一个示例。更完善的方案是使用TLS(HTTPS)加密整个通信链路,并结合CSRF Token等防止重放攻击。前端哈希的主要作用是避免密码在传输过程中因某些中间环节(如日志)泄露而直接暴露明文。
5.2 场景二:本地敏感数据的加密存储
目标:在浏览器的localStorage或IndexedDB中存储用户笔记、草稿等数据,即使数据被窃取,没有密码也无法解密。 方案:使用用户提供的口令(passphrase)派生出一个加密密钥,用该密钥加密数据后存储。
// 使用用户输入的口令加密数据 function encryptDataForLocalStorage(plainText, userPassphrase) { // 1. 生成一个随机的盐(Salt)和IV。盐用于从口令派生密钥,IV用于加密。 const salt = CryptoJS.lib.WordArray.random(128/8); // 16字节盐 const iv = CryptoJS.lib.WordArray.random(128/8); // 16字节IV (AES-128 CBC) // 2. 使用PBKDF2从口令和盐派生出一个固定长度的密钥 // 注意:keySize是“字”数(1字=4字节),AES-128需要4个字(16字节) const key = CryptoJS.PBKDF2(userPassphrase, salt, { keySize: 128 / 32, // 4个字,16字节 iterations: 10000, hasher: CryptoJS.algo.SHA256 }); // 3. 使用派生出的密钥和IV加密数据 const encrypted = CryptoJS.AES.encrypt(plainText, key, { iv: iv }); // 4. 将盐、IV和密文打包在一起存储。盐和IV不是秘密,可以公开。 const dataToStore = { ct: encrypted.toString(), // 密文 (CipherText) iv: iv.toString(CryptoJS.enc.Hex), s: salt.toString(CryptoJS.enc.Hex) }; return JSON.stringify(dataToStore); } // 解密函数 function decryptDataFromLocalStorage(encryptedDataJson, userPassphrase) { const data = JSON.parse(encryptedDataJson); const salt = CryptoJS.enc.Hex.parse(data.s); const iv = CryptoJS.enc.Hex.parse(data.iv); const cipherText = data.ct; // 重新用相同的口令和盐派生密钥 const key = CryptoJS.PBKDF2(userPassphrase, salt, { keySize: 128 / 32, iterations: 10000, hasher: CryptoJS.algo.SHA256 }); // 解密 const decrypted = CryptoJS.AES.decrypt(cipherText, key, { iv: iv }); return decrypted.toString(CryptoJS.enc.Utf8); } // 使用示例 const mySecretNote = “我的银行密码是...(开玩笑的)”; const userPassword = “AStrongUserPassword!”; const encryptedPackage = encryptDataForLocalStorage(mySecretNote, userPassword); localStorage.setItem(“encryptedNote”, encryptedPackage); console.log(“已加密存储:”, encryptedPackage); // 稍后读取 const storedPackage = localStorage.getItem(“encryptedNote”); try { const decryptedNote = decryptDataFromLocalStorage(storedPackage, userPassword); console.log(“解密后的笔记:”, decryptedNote); } catch (e) { console.error(“解密失败!可能是密码错误或数据损坏。”, e); }这个方案的安全性依赖于用户口令的强度。迭代次数(iterations)可以调高(如10万次)以增加暴力破解的难度,但会稍微影响前端性能。
5.3 场景三:与后端交互的加密通信封装
目标:对发送给后端的特定敏感字段进行加密,后端用对应的私钥解密。 方案:前端使用后端提供的公钥(或预共享的对称密钥)加密数据。这里演示更常见的对称密钥方案,假设密钥已通过安全渠道交换。
// 假设我们有一个与后端共享的AES密钥(实际中应由后端生成并在安全会话中下发) let sharedEncryptionKey = null; // 从安全接口获取后填充 // 封装一个加密的fetch请求 async function encryptedPost(url, plainDataObject) { if (!sharedEncryptionKey) { await fetchSharedKey(); // 这是一个获取密钥的假设函数 } const iv = CryptoJS.lib.WordArray.random(16); // 将数据对象转换为JSON字符串进行加密 const plainText = JSON.stringify(plainDataObject); const encrypted = CryptoJS.AES.encrypt(plainText, sharedEncryptionKey, { iv: iv }); // 将IV和密文一起发送 const payload = { iv: iv.toString(CryptoJS.enc.Base64), data: encrypted.toString() // 密文本身就是Base64字符串 }; const response = await fetch(url, { method: ‘POST’, headers: { ‘Content-Type’: ‘application/json’ }, body: JSON.stringify(payload) }); return response.json(); // 假设后端返回的也是加密的JSON,需要类似解密流程 } // 对应的后端(以Node.js为例)解密逻辑伪代码: /* const crypto = require(‘crypto’); function decryptRequestBody(reqBody) { const { iv, data } = reqBody; const decipher = crypto.createDecipheriv(‘aes-256-cbc’, sharedEncryptionKeyBuffer, Buffer.from(iv, ‘base64’)); let decrypted = decipher.update(data, ‘base64’, ‘utf8’); decrypted += decipher.final(‘utf8’); return JSON.parse(decrypted); } */这种模式确保了即使在HTTPS之上,敏感数据在应用层也有一层额外的加密。但请注意,密钥管理是最大的挑战,需要设计安全的密钥分发和轮换机制。
6. 常见陷阱、错误排查与性能优化
即使理解了原理,在实际编码中你仍会遇到各种问题。下面是一些高频陷阱和解决方案。
6.1 错误排查速查表
| 错误现象或问题 | 可能原因 | 解决方案 |
|---|---|---|
Malformed UTF-8 data | 1. 尝试解密一个不是有效Base64/Hex的字符串。 2. 解密后结果不是有效的UTF-8字节序列(密码/密钥错误导致解密出乱码)。 | 1. 检查密文字符串是否完整、未被篡改,确认编码(是Base64还是Hex)。 2. 确认使用的密钥、IV、模式、填充是否与加密时完全一致。密钥错误是最常见原因。 |
Invalid key length | 提供的密钥长度不符合算法要求。例如,AES-128需要16字节密钥,你提供的字符串UTF-8编码后可能不是16字节。 | 使用CryptoJS.enc.Utf8.parse(yourKeyString).sigBytes检查字节长度。使用PBKDF2从口令派生固定长度密钥。 |
| 解密结果为空字符串 | 解密过程没有报错,但toString()结果为空。 | 通常是密钥或IV错误,导致解密出的WordArray是空的或全是0。先检查密钥和IV的编码和值。可以尝试console.log(decrypted)查看解密出的WordArray对象内容。 |
| 跨语言加解密不一致 | 前端用CryptoJS加密,后端(如Java/Python/PHP)解密失败。 | 确保两端以下参数完全匹配: 1.算法(AES) 2.密钥长度(128/192/256) 3.模式(CBC/ECB/GCM等) 4.填充(PKCS#7/PKCS#5) 5.IV(值及编码) 6.密钥和数据的编码(UTF-8/Base64/Hex) |
Uncaught TypeError: Cannot read properties of undefined | 引入CryptoJS的方式不对,或模块路径错误,CryptoJS对象未定义。 | 检查<script>标签的CDN链接是否正确,或检查ES6导入语句路径。在代码前加console.log(CryptoJS)测试是否加载成功。 |
6.2 性能与安全注意事项
- 前端加密不能替代HTTPS: CryptoJS等前端库的加密,其密钥和代码都暴露在用户浏览器中,无法防止中间人攻击。HTTPS(TLS)是保障传输层安全的基石,绝对不可省略。前端加密主要用于“传输过程中不出现明文”、“客户端存储加密”等附加安全层。
- 避免加密大文件: 加密解密是CPU密集型操作。在前端加密数MB以上的文件会导致页面卡顿,用户体验差。对于大文件,考虑分片加密或提示用户文件过大。
- 密钥生命周期管理: 硬编码在JS文件中的密钥是无效的加密。密钥应该由后端在安全会话中动态生成并提供给前端(例如,用户登录后通过HTTPS下发一个临时会话加密密钥),并且定期更换。
- 使用强随机数:
CryptoJS.lib.WordArray.random()是基于浏览器crypto.getRandomValues的,是密码学安全的随机数生成器(CSPRNG)。永远不要用Math.random()来生成密钥或IV。 - 考虑使用Web Crypto API: 对于现代浏览器,原生的Web Crypto API提供了更优的性能和安全性,并且是W3C标准。CryptoJS可以作为一个兼容性垫片(polyfill)或用于需要特定算法的场景。如果项目只面向现代浏览器,评估直接使用Web Crypto API是值得的。
6.3 调试技巧
- 分步输出: 在加密和解密的每一步,都
console.log输出关键中间值(如key.toString(CryptoJS.enc.Hex),iv, 加密后的cipherParams对象)。对比加密和解密过程中的这些值是否一致。 - 使用已知答案测试: 找一些在线的AES加密工具,用相同的密钥、IV、模式和明文进行加密,比对CryptoJS的输出是否与工具一致。这能快速定位是参数问题还是代码逻辑问题。
- 关注控制台警告: 某些浏览器扩展可能会干扰
crypto.getRandomValues,导致随机数质量下降。在极度敏感的应用中需要注意。
7. 进阶话题:模式、填充与Web Crypto API
当你熟悉了基础用法,可以探索一些更深入的内容来优化安全性和性能。
7.1 探索其他加密模式:GCM
CBC模式需要单独的消息认证,而GCM(Galois/Counter Mode)是一种AEAD(认证加密关联数据)模式,它同时提供加密和完整性认证。使用GCM可以避免“加密但不验证”导致的数据可能被篡改而无法察觉的风险。
// CryptoJS中使用GCM模式 const plaintext = “机密数据”; const key = CryptoJS.lib.WordArray.random(256/8); // 32字节密钥 const iv = CryptoJS.lib.WordArray.random(96/8); // GCM通常推荐12字节IV const encrypted = CryptoJS.AES.encrypt(plaintext, key, { iv: iv, mode: CryptoJS.mode.GCM, // GCM模式可以添加附加认证数据(AAD),此处未使用 }); // GCM加密输出包含密文和认证标签(tag) const cipherText = encrypted.ciphertext.toString(CryptoJS.enc.Base64); const tag = encrypted.tag.toString(CryptoJS.enc.Base64); // 需要将tag也存储或传输 console.log(`密文: ${cipherText}`); console.log(`认证标签: ${tag}`); // 解密时需要提供tag const decrypted = CryptoJS.AES.decrypt( { ciphertext: CryptoJS.enc.Base64.parse(cipherText) }, // 需要重新组装成对象 key, { iv: iv, mode: CryptoJS.mode.GCM, tag: CryptoJS.enc.Base64.parse(tag) // 传入tag } ); console.log(“解密:”, decrypted.toString(CryptoJS.enc.Utf8));GCM模式更安全,且通常比“CBC+HMAC”的组合更高效。但它对IV的唯一性要求更为严格,重复使用IV会导致灾难性的安全漏洞。
7.2 与原生Web Crypto API的对比与互操作
Web Crypto API是浏览器内置的加密标准,性能更好,并且密钥可以保存在更安全的密钥存储中(如非提取密钥)。以下是使用Web Crypto API进行AES-GCM加密的简要示例:
async function encryptWithWebCrypto(plaintext, keyMaterial) { const encoder = new TextEncoder(); const data = encoder.encode(plaintext); // 生成密钥(从原始密钥材料导入) const key = await window.crypto.subtle.importKey( ‘raw’, encoder.encode(keyMaterial), ‘AES-GCM’, false, [‘encrypt’, ‘decrypt’] ); const iv = window.crypto.getRandomValues(new Uint8Array(12)); // 12字节IV for GCM const encrypted = await window.crypto.subtle.encrypt( { name: ‘AES-GCM’, iv: iv }, key, data ); // 将ArrayBuffer转换为Base64以便传输 const ciphertext = btoa(String.fromCharCode(...new Uint8Array(encrypted))); const ivBase64 = btoa(String.fromCharCode(...iv)); return { ciphertext, iv: ivBase64 }; }如何选择?
- 使用CryptoJS: 需要支持旧浏览器(如IE10)、需要特定算法(如DES、RC4,虽然不推荐)、或者项目已深度依赖CryptoJS。
- 使用Web Crypto API: 面向现代浏览器、追求最佳性能和安全性、希望使用密钥库功能。
两者可以共存。例如,用Web Crypto API做主要加密,用CryptoJS作为旧版浏览器的回退方案。
7.3 自定义编码与格式处理
有时你需要处理特殊格式的数据。CryptoJS支持在WordArray和多种格式间转换。
// 示例:处理ArrayBuffer(例如来自FileReader) function arrayBufferToWordArray(ab) { const byteArray = new Uint8Array(ab); const words = []; for (let i = 0; i < byteArray.length; i += 4) { let word = 0; for (let j = 0; j < 4 && i + j < byteArray.length; j++) { word |= byteArray[i + j] << (24 - j * 8); } words.push(word); } return CryptoJS.lib.WordArray.create(words, byteArray.length); } // 示例:加密一个文件(前1MB) fileInput.addEventListener(‘change’, async (e) => { const file = e.target.files[0]; const slice = file.slice(0, 1024 * 1024); // 取前1MB const arrayBuffer = await slice.arrayBuffer(); const fileDataWordArray = arrayBufferToWordArray(arrayBuffer); const key = CryptoJS.enc.Utf8.parse(“a-16-byte-key!!”); const iv = CryptoJS.lib.WordArray.random(16); // 注意:直接加密大型WordArray可能内存压力大,生产环境应分块处理 const encrypted = CryptoJS.AES.encrypt(fileDataWordArray, key, { iv: iv }); // encrypted.ciphertext 是一个WordArray,可以转换回ArrayBuffer或Base64 const encryptedBase64 = encrypted.toString(); console.log(“文件部分加密完成:”, encryptedBase64.substring(0, 100) + “...”); });走到这里,你已经从“知道怎么调用CryptoJS.AES.encrypt”升级到了“理解其内部原理并能根据场景灵活运用和排错”。记住,加密是一个严谨的领域,细节决定成败。始终使用经过验证的模式(如AES-GCM、CBC+HMAC),管理好你的密钥和IV,并用HTTPS为你的安全大厦打下最坚固的地基。在实际项目中,多写测试用例,确保加密解密循环的健壮性,这样当真正需要保护用户数据时,你才能心中有底,手下不慌。