
本文详解如何基于用户密码(pbkdf2 密钥派生)生成 aes-gcm 密钥,并完整实现加密与解密流程,重点解决 iv 和 salt 的嵌入、复用及跨会话一致性问题。
本文详解如何基于用户密码(pbkdf2 密钥派生)生成 aes-gcm 密钥,并完整实现加密与解密流程,重点解决 iv 和 salt 的嵌入、复用及跨会话一致性问题。
在 Web 端使用 AES-GCM 进行密码学安全的对称加密时,仅凭密码无法直接加密/解密——必须通过密钥派生函数(如 PBKDF2)将密码转化为强加密密钥。而原始代码失败的根本原因在于:加密时随机生成了 Salt,但解密时未保存并复用该 Salt,导致 deriveKey 产生完全不同的 AES 密钥,解密必然失败。
✅ 正确方案的核心原则是:Salt 必须与密文一同持久化,并在解密时精确复用。以下是经过验证的完整实现:
? 密钥派生函数(带 Salt 复用支持)
async function genAESKeyFromPassword(password, salt) {
const encoder = new TextEncoder();
const passwordKey = await crypto.subtle.importKey(
'raw',
encoder.encode(password),
'PBKDF2',
false,
['deriveKey']
);
return await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 100_000, // 推荐 ≥100,000,兼顾安全与性能
hash: 'SHA-256'
},
passwordKey,
{ name: 'AES-GCM', length: 256 },
true,
['encrypt', 'decrypt']
);
}⚠️ 注意:iterations 值必须加密与解密完全一致;生产环境建议使用 100000 或更高(如 300000),避免暴力破解。
Alibabacloud Sdk Client Initialization For Java下载在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
? 加密流程(嵌入 Salt + IV + Ciphertext)
async function encryptText(password, plaintext) {
const encoder = new TextEncoder();
const data = encoder.encode(plaintext);
const iv = crypto.getRandomValues(new Uint8Array(12)); // GCM 标准 IV 长度为 12 字节
const salt = crypto.getRandomValues(new Uint8Array(16)); // PBKDF2 推荐 Salt ≥16 字节
const key = await genAESKeyFromPassword(password, salt);
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
data
);
// 按顺序拼接:[salt(16)][iv(12)][ciphertext]
const combined = new Uint8Array(16 + 12 + ciphertext.byteLength);
combined.set(salt, 0);
combined.set(iv, 16);
combined.set(new Uint8Array(ciphertext), 28);
return btoa(String.fromCharCode(...combined));
}? 解密流程(从密文中提取 Salt & IV 并复用)
async function decryptText(password, base64Cipher) {
const bytes = atob(base64Cipher)
.split('')
.map(char => char.charCodeAt(0));
const combined = new Uint8Array(bytes);
if (combined.length < 28) throw new Error('Invalid cipher: too short');
const salt = combined.slice(0, 16);
const iv = combined.slice(16, 28);
const ciphertext = combined.slice(28);
const key = await genAESKeyFromPassword(password, salt);
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv },
key,
ciphertext
);
return new TextDecoder().decode(decrypted);
}? 完整 HTML 示例(含表单交互)
<form id="encrypt">
<h2>? 加密</h2>
<p><input type="text" class="text" placeholder="输入明文"></p>
<p><input type="password" class="password" placeholder="输入密码"></p>
<p><button type="submit">执行加密</button> <span class="result"></span></p>
</form>
<form id="decrypt">
<h2>? 解密</h2>
<p><input type="text" class="text" placeholder="粘贴 Base64 密文"></p>
<p><input type="password" class="password" placeholder="输入相同密码"></p>
<p><button type="submit">执行解密</button> <span class="result"></span></p>
</form>
<script>
document.getElementById('encrypt').addEventListener('submit', async e => {
e.preventDefault();
const text = e.target.querySelector('.text').value;
const pwd = e.target.querySelector('.password').value;
const res = e.target.querySelector('.result');
try {
res.textContent = await encryptText(pwd, text);
} catch (err) {
res.textContent = '加密失败: ' + err.message;
}
});
document.getElementById('decrypt').addEventListener('submit', async e => {
e.preventDefault();
const cipher = e.target.querySelector('.text').value;
const pwd = e.target.querySelector('.password').value;
const res = e.target.querySelector('.result');
try {
res.textContent = await decryptText(pwd, cipher);
} catch (err) {
res.textContent = '解密失败: ' + err.message;
}
});
</script>✅ 关键总结
- Salt 不可省略,且必须随密文存储:它是密码派生的唯一性保障,缺失或错位将导致密钥不匹配;
- IV 必须唯一(不可重复使用同一 IV 加密多条消息),但可公开传输,故与 Salt 一同嵌入密文;
- Base64 编码/解码需严格处理二进制:使用 atob + charCodeAt 或更健壮的 Uint8Array.from(atob(...), c => c.codePointAt(0));
- 错误处理必不可少:GCM 解密失败(如密钥错误、IV 错误、篡改)会抛出异常,不可静默忽略;
- 生产环境增强建议:添加认证标签(GCM 自带)、前端加盐哈希预校验、限制解密尝试次数防暴力。
遵循以上结构,即可在浏览器中安全、可靠地实现基于密码的 AES-GCM 加解密。
立即学习“Java免费学习笔记(深入)”;


















