各位观众老爷们,晚上好! 咱们今天来聊聊 Web Crypto API 这玩意儿。别看它名字挺唬人,好像很高深莫测的样子,其实用好了能帮你加固网站安全,用不好嘛…嘿嘿,那就等着黑客蜀黍上门拜访吧。
咱们今天的主题是:Web Crypto API 的正确使用:避免常见的加密误用漏洞。
我会尽量用大白话,配合代码示例,让大家都能听明白,并且能避免一些常见的坑。准备好了吗?咱们这就开始!
第一部分:Web Crypto API 是个啥?
简单来说,Web Crypto API 就是浏览器提供的一套用于执行加密操作的 JavaScript API。它允许你在客户端执行诸如生成密钥、加密数据、签名数据等操作,而无需依赖服务器。
想象一下,你和妹子用微信聊天,内容需要加密才能保证不被别人偷窥,那么 Web Crypto API 就相当于微信自带的加密引擎,帮你把聊天内容变成只有你和妹子才能看懂的火星文。
第二部分:Web Crypto API 的基本概念
在使用 Web Crypto API 之前,我们需要了解几个核心概念:
- 算法 (Algorithm): 加密/解密、签名/验证的具体方法,比如 AES、RSA、SHA-256 等。 不同的算法适用于不同的场景。
- 密钥 (Key): 用于加密/解密或签名/验证的秘密信息。 密钥的安全性至关重要。
- 密文 (Ciphertext): 经过加密后的数据,是人类无法直接阅读的内容。
- 明文 (Plaintext): 原始的、未加密的数据,是人类可以阅读的内容。
- 向量 (IV) / Salt: 用于增加加密随机性的数据,防止相同的明文生成相同的密文。
- 哈希 (Hash): 将任意长度的数据转换为固定长度的唯一值。 用于数据完整性校验。
咱们用一个表格来总结一下:
概念 | 解释 | 举例 |
---|---|---|
算法 | 用于加密/解密、签名/验证的具体方法。 | AES-CBC, RSA-PKCS1.5, SHA-256 |
密钥 | 用于加密/解密或签名/验证的秘密信息。 | 一串随机的二进制数据 |
密文 | 经过加密后的数据。 | AWESOMESAUCE 加密后变成 j#$@%^&*() |
明文 | 原始的、未加密的数据。 | AWESOMESAUCE |
向量/Salt | 用于增加加密随机性的数据。 | 一串随机的二进制数据 |
哈希 | 将任意长度的数据转换为固定长度的唯一值。 | AWESOMESAUCE 哈希后变成 e5b70398397f4474724e22e6089607d3 |
第三部分:Web Crypto API 的基本用法
Web Crypto API 的入口点是 window.crypto.subtle
对象。 通过它,我们可以访问各种加密操作。
1. 生成密钥 (Key Generation)
首先,我们需要生成密钥。不同的算法需要不同类型的密钥。
- 对称密钥 (Symmetric Key): 加密和解密使用相同的密钥,例如 AES。
- 非对称密钥 (Asymmetric Key): 加密和解密使用不同的密钥,例如 RSA。 一个是公钥 (Public Key),用于加密或验证签名;另一个是私钥 (Private Key),用于解密或生成签名。
代码示例 (AES 密钥生成):
async function generateAESKey() {
try {
const key = await window.crypto.subtle.generateKey(
{
name: "AES-CBC",
length: 256, // 密钥长度,可以是 128, 192, 或 256
},
true, // 是否可以导出密钥
["encrypt", "decrypt"] // 密钥用途,可以是 encrypt, decrypt, sign, verify, wrapKey, unwrapKey
);
return key;
} catch (error) {
console.error("密钥生成失败:", error);
return null;
}
}
generateAESKey().then(key => {
if (key) {
console.log("AES 密钥生成成功:", key);
}
});
代码解释:
window.crypto.subtle.generateKey()
: 用于生成密钥的函数。{ name: "AES-CBC", length: 256 }
: 指定算法和密钥长度。 这里我们使用 AES-CBC 算法,密钥长度为 256 位。true
: 指定密钥是否可以导出。如果设置为true
,你可以使用exportKey()
函数将密钥导出为 JSON Web Key (JWK) 格式。 注意:除非绝对必要,否则不要导出密钥! 导出密钥会增加密钥泄露的风险。["encrypt", "decrypt"]
: 指定密钥的用途。 这里我们指定密钥用于加密和解密。
代码示例 (RSA 密钥生成):
async function generateRSAKey() {
try {
const keyPair = await window.crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 2048, // 密钥长度,至少 2048 位
publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 公钥指数,通常是 65537
hash: "SHA-256", // 哈希算法
},
true, // 是否可以导出密钥
["encrypt", "decrypt"] // 密钥用途
);
return keyPair;
} catch (error) {
console.error("密钥生成失败:", error);
return null;
}
}
generateRSAKey().then(keyPair => {
if (keyPair) {
console.log("RSA 密钥生成成功:", keyPair);
}
});
代码解释:
{ name: "RSA-OAEP", ... }
: 指定 RSA-OAEP 算法,并设置密钥长度、公钥指数和哈希算法。modulusLength
: RSA 密钥长度,通常是 2048 位或更高。 密钥长度越长,安全性越高,但计算速度也越慢。publicExponent
: 公钥指数,通常是 65537 (0x010001)。hash
: 哈希算法,用于 OAEP 填充方案。
2. 加密 (Encryption)
有了密钥,我们就可以对数据进行加密了。
代码示例 (AES 加密):
async function encryptAES(key, plaintext) {
try {
const iv = window.crypto.getRandomValues(new Uint8Array(16)); // 生成随机向量 (IV)
const ciphertext = await window.crypto.subtle.encrypt(
{
name: "AES-CBC",
iv: iv, // 使用向量
},
key,
new TextEncoder().encode(plaintext) // 将明文转换为 Uint8Array
);
// 将 IV 和密文合并,方便存储和传输
const combined = new Uint8Array(iv.length + ciphertext.byteLength);
combined.set(iv, 0);
combined.set(new Uint8Array(ciphertext), iv.length);
return combined;
} catch (error) {
console.error("加密失败:", error);
return null;
}
}
// 使用示例
generateAESKey().then(key => {
if (key) {
const plaintext = "AWESOMESAUCE";
encryptAES(key, plaintext).then(ciphertext => {
if (ciphertext) {
console.log("加密后的数据:", ciphertext);
}
});
}
});
代码解释:
window.crypto.getRandomValues(new Uint8Array(16))
: 生成 16 字节的随机向量 (IV)。 IV 必须是随机的,且每次加密都应该生成新的 IV。{ name: "AES-CBC", iv: iv }
: 指定算法和向量。new TextEncoder().encode(plaintext)
: 将明文转换为Uint8Array
格式。 Web Crypto API 只能处理ArrayBuffer
或Uint8Array
格式的数据。combined.set(iv, 0); combined.set(new Uint8Array(ciphertext), iv.length);
: 将 IV 和密文合并。 这是因为解密时需要用到 IV。
代码示例 (RSA 加密):
async function encryptRSA(publicKey, plaintext) {
try {
const ciphertext = await window.crypto.subtle.encrypt(
{
name: "RSA-OAEP",
},
publicKey,
new TextEncoder().encode(plaintext)
);
return ciphertext;
} catch (error) {
console.error("加密失败:", error);
return null;
}
}
// 使用示例
generateRSAKey().then(keyPair => {
if (keyPair) {
const publicKey = keyPair.publicKey;
const plaintext = "AWESOMESAUCE";
encryptRSA(publicKey, plaintext).then(ciphertext => {
if (ciphertext) {
console.log("加密后的数据:", ciphertext);
}
});
}
});
代码解释:
- RSA 加密使用公钥进行加密。
3. 解密 (Decryption)
解密是加密的逆过程。
代码示例 (AES 解密):
async function decryptAES(key, combined) {
try {
const iv = combined.slice(0, 16); // 从合并后的数据中提取 IV
const ciphertext = combined.slice(16); // 从合并后的数据中提取密文
const plaintext = await window.crypto.subtle.decrypt(
{
name: "AES-CBC",
iv: iv,
},
key,
ciphertext
);
return new TextDecoder().decode(plaintext); // 将解密后的数据转换为字符串
} catch (error) {
console.error("解密失败:", error);
return null;
}
}
// 使用示例
generateAESKey().then(key => {
if (key) {
const plaintext = "AWESOMESAUCE";
encryptAES(key, plaintext).then(ciphertext => {
if (ciphertext) {
decryptAES(key, ciphertext).then(decryptedText => {
console.log("解密后的数据:", decryptedText); // 输出 "AWESOMESAUCE"
});
}
});
}
});
代码解释:
combined.slice(0, 16); combined.slice(16);
: 从合并后的数据中提取 IV 和密文。new TextDecoder().decode(plaintext)
: 将解密后的ArrayBuffer
转换为字符串。
代码示例 (RSA 解密):
async function decryptRSA(privateKey, ciphertext) {
try {
const plaintext = await window.crypto.subtle.decrypt(
{
name: "RSA-OAEP",
},
privateKey,
ciphertext
);
return new TextDecoder().decode(plaintext);
} catch (error) {
console.error("解密失败:", error);
return null;
}
}
// 使用示例
generateRSAKey().then(keyPair => {
if (keyPair) {
const privateKey = keyPair.privateKey;
const publicKey = keyPair.publicKey;
const plaintext = "AWESOMESAUCE";
encryptRSA(publicKey, plaintext).then(ciphertext => {
if (ciphertext) {
decryptRSA(privateKey, ciphertext).then(decryptedText => {
console.log("解密后的数据:", decryptedText); // 输出 "AWESOMESAUCE"
});
}
});
}
});
代码解释:
- RSA 解密使用私钥进行解密。
4. 签名 (Signing) 和 验证 (Verification)
数字签名用于验证数据的完整性和来源。
代码示例 (HMAC 签名和验证):
async function signHMAC(key, data) {
try {
const signature = await window.crypto.subtle.sign(
{
name: "HMAC",
hash: "SHA-256", // 指定哈希算法
},
key,
new TextEncoder().encode(data)
);
return signature;
} catch (error) {
console.error("签名失败:", error);
return null;
}
}
async function verifyHMAC(key, signature, data) {
try {
const isValid = await window.crypto.subtle.verify(
{
name: "HMAC",
hash: "SHA-256",
},
key,
signature,
new TextEncoder().encode(data)
);
return isValid;
} catch (error) {
console.error("验证失败:", error);
return false;
}
}
// 使用示例
async function main() {
const key = await window.crypto.subtle.generateKey(
{
name: "HMAC",
hash: "SHA-256",
},
true,
["sign", "verify"]
);
const data = "This is the data to be signed.";
const signature = await signHMAC(key, data);
if (signature) {
const isValid = await verifyHMAC(key, signature, data);
console.log("签名是否有效:", isValid); // 输出 true
// 篡改数据
const tamperedData = "This is the tampered data.";
const isStillValid = await verifyHMAC(key, signature, tamperedData);
console.log("篡改后的签名是否有效:", isStillValid); // 输出 false
}
}
main();
代码解释:
HMAC
算法使用密钥和哈希函数来生成签名。- 签名验证需要使用相同的密钥、哈希函数和数据。
- 如果数据被篡改,签名验证将会失败。
第四部分:常见的加密误用漏洞及防范措施
好了,基本用法咱们都过了一遍。接下来,咱们来聊聊一些常见的坑,以及如何避免掉进去。
1. 使用不安全的算法或模式
- 问题: 使用已经过时或存在安全漏洞的算法或模式,例如 DES, RC4, MD5, SHA-1, CBC 模式不使用 authenticated encryption (AEAD)。
- 防范措施:
- 坚持使用最新、最安全的算法和模式。 推荐使用 AES-GCM (Authenticated Encryption with Associated Data), ChaCha20-Poly1305, RSA-OAEP, ECDSA, Ed25519 等。
- 避免使用 CBC 模式,除非你非常清楚自己在做什么,并且使用了 authenticated encryption (AEAD)。 CBC 模式容易受到 padding oracle 攻击。
- 定期更新你的加密库,以修复已知的安全漏洞。 虽然 Web Crypto API 是浏览器提供的,但是浏览器也会定期更新,所以要关注浏览器的更新说明。
2. 密钥管理不当
- 问题: 将密钥存储在客户端,或者使用弱密钥、硬编码密钥。
- 防范措施:
- 永远不要将密钥存储在客户端! 客户端是不安全的,任何存储在客户端的数据都可能被窃取。
- 使用服务器端密钥管理系统来存储和管理密钥。
- 使用强密钥。 密钥长度越长,安全性越高。 对于 AES,推荐使用 256 位密钥。 对于 RSA,推荐使用 2048 位或更高位密钥。
- 使用密码学安全的随机数生成器来生成密钥。 Web Crypto API 的
window.crypto.getRandomValues()
函数就是一个密码学安全的随机数生成器。 - 定期更换密钥。
3. 向量 (IV) / Salt 使用不当
- 问题: 使用固定 IV, 重复使用 IV, 或者没有使用足够随机的 Salt。
- 防范措施:
- 对于 CBC 模式,必须使用随机的、唯一的 IV。 不要使用固定 IV 或重复使用 IV。
- 对于密码哈希,必须使用随机的 Salt。 Salt 用于增加哈希的复杂度,防止彩虹表攻击。
- IV 和 Salt 应该足够长。 对于 AES-CBC,IV 应该是 16 字节。 Salt 至少应该是 16 字节。
4. 错误处理不当
- 问题: 忽略加密操作中的错误,或者没有正确处理错误。
- 防范措施:
- 使用
try...catch
语句来捕获加密操作中的错误。 - 在发生错误时,记录错误信息,并采取适当的措施,例如重新生成密钥或停止加密操作。
- 不要向用户显示详细的错误信息,以防止攻击者利用这些信息来攻击你的系统。
- 使用
5. 混淆加密和哈希
- 问题: 错误地使用哈希来代替加密。
- 防范措施:
- 加密用于保护数据的机密性,而哈希用于验证数据的完整性。
- 不要使用哈希来存储密码。 应该使用加盐哈希函数,例如 bcrypt, scrypt, Argon2。
- 不要使用哈希来代替加密。 哈希是单向的,无法将哈希值还原为原始数据。
6. 缺乏安全审计
- 问题: 没有定期进行安全审计,以发现和修复加密相关的安全漏洞。
- 防范措施:
- 定期进行安全审计,以发现和修复加密相关的安全漏洞。
- 使用静态代码分析工具来检查代码中是否存在加密相关的安全漏洞。
- 进行渗透测试,以模拟攻击者攻击你的系统,并发现安全漏洞。
7. 跨域问题
- 问题: 在不同的域名之间共享密钥或密文,导致跨域安全问题。
- 防范措施:
- 使用 CORS (Cross-Origin Resource Sharing) 来控制跨域访问。
- 避免在不同的域名之间共享密钥或密文。
- 使用安全的跨域通信机制,例如 postMessage。
第五部分:实战演练:安全存储密码
咱们来一个实际的例子,看看如何使用 Web Crypto API 来安全地存储密码。
错误示范:
// 绝对不要这样做!
function storePassword(password) {
localStorage.setItem("password", password); // 明文存储密码,太可怕了!
}
正确示范 (使用 bcrypt 加盐哈希):
由于 Web Crypto API 本身并没有提供 bcrypt 算法,我们需要借助第三方库来实现。这里我们使用 bcryptjs
库。
// 首先,引入 bcryptjs 库
// 可以通过 npm 安装: npm install bcryptjs
import bcrypt from 'bcryptjs';
async function hashPassword(password) {
const saltRounds = 10; // Salt 的轮数,越大越安全,但计算时间也越长
const hash = await bcrypt.hash(password, saltRounds);
return hash;
}
async function verifyPassword(password, hash) {
const match = await bcrypt.compare(password, hash);
return match;
}
// 使用示例
async function main() {
const password = "AWESOMESAUCE";
const hashedPassword = await hashPassword(password);
console.log("哈希后的密码:", hashedPassword);
const isValid = await verifyPassword(password, hashedPassword);
console.log("密码是否匹配:", isValid); // 输出 true
const invalidPassword = "WRONGPASSWORD";
const isStillValid = await verifyPassword(invalidPassword, hashedPassword);
console.log("错误的密码是否匹配:", isStillValid); // 输出 false
}
main();
代码解释:
bcrypt.hash(password, saltRounds)
: 使用 bcrypt 算法对密码进行加盐哈希。bcrypt.compare(password, hash)
: 验证密码是否与哈希值匹配。
注意: bcryptjs
库是纯 JavaScript 实现的,性能可能不如原生实现的 bcrypt 库。 如果对性能有要求,可以考虑使用其他 bcrypt 库,例如 bcrypt
(需要 native bindings)。
第六部分:总结
Web Crypto API 是一把双刃剑,用好了可以保护你的数据安全,用不好就会留下安全漏洞。 记住以下几点:
- 选择安全的算法和模式。
- 妥善管理密钥。
- 使用随机的 IV 和 Salt。
- 正确处理错误。
- 不要混淆加密和哈希。
- 定期进行安全审计。
希望今天的讲座对大家有所帮助。 记住,安全无小事,时刻保持警惕,才能构建更安全的 Web 应用。
溜了溜了,下次再见!