后端AES-256-GCM加密内容,前端解密失败求排查方案
AES-256-GCM 前后端加解密失败排查与解决方案
问题背景
我正在开发一个全栈项目,后端使用Node.js的crypto模块实现AES-256-GCM对称加密逻辑,加密后将iv、加密内容、tag传递到前端;前端通过Web Crypto API尝试解密,传入相同的iv、tag和密钥,但解密失败且无具体错误,调试确认代码执行到decrypt函数前均正常。
后端原加密代码
import { randomBytes, createCipheriv } from "crypto"; const encryptMessage = (message, credential) => { const key = Buffer.from(credential, "utf-8"); const iv = randomBytes(16); const cipher = createCipheriv("aes-256-gcm", key, iv); const encryptedMessage = Buffer.concat([ cipher.update(message, "utf-8"), cipher.final(), ]); const tag = cipher.getAuthTag(); return { iv: iv.toString("hex"), encryptedMessage: encryptedMessage.toString("hex"), tag: tag.toString("hex"), }; }; export default encryptMessage;
前端原解密代码
const decryptMessage = async (encryptedData, credential) => { const decoder = new TextDecoder(); const iv = new Uint8Array(encryptedData.iv.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))); const encryptedMessage = new Uint8Array(encryptedData.encryptedMessage.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))); const tag = new Uint8Array(encryptedData.tag.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))); const encodedCredential =new TextEncoder().encode(credential) try{ const key = await crypto.subtle.importKey( "raw", encodedCredential, { name: "AES-GCM" }, false, ["decrypt"] ); const decrypted = await crypto.subtle.decrypt( { name: "AES-GCM", iv: iv, tag: tag }, key, encryptedMessage ); const decryptedMessage = decoder.decode(decrypted); console.log("Decrypted Message:", decryptedMessage); return decryptedMessage; }catch(error){ console.error("Decryption Error:", error); throw new Error ("Decryption Failed" + error.message); } } export default decryptMessage
原调用方式
const privateKey = await decryptMessage(keys, credential);
错误原因分析
- 密钥长度不匹配:AES-256要求密钥必须是32字节(256位)。后端代码直接将
credential转成Buffer作为密钥,若credential的UTF-8编码长度不是32字节,Node.js的crypto模块会自动截断为32字节;但前端Web Crypto API会严格校验密钥长度,长度不符直接触发解密失败。 - 错误信息被掩盖:前端catch块中仅抛出拼接后的模糊错误,未打印完整的
error对象,导致无法获取具体失败原因(比如密钥长度错误、tag验证失败等)。
修复方案(可行的前后端加解密实现)
核心调整:统一密钥派生逻辑
不要直接用明文密码作为AES密钥,改用PBKDF2密钥派生函数从用户密码生成固定32字节的AES-256密钥,前后端保持完全一致的派生规则。
后端修复代码
import { randomBytes, createCipheriv, pbkdf2Sync } from "crypto"; // 加密函数:每次加密生成随机盐,确保安全性 const encryptMessage = (message, password) => { const salt = randomBytes(16); // 用PBKDF2派生32字节AES-256密钥 const key = pbkdf2Sync(password, salt, 100000, 32, "sha256"); const iv = randomBytes(16); const cipher = createCipheriv("aes-256-gcm", key, iv); const encryptedMessage = Buffer.concat([cipher.update(message, "utf-8"), cipher.final()]); const tag = cipher.getAuthTag(); return { iv: iv.toString("hex"), encryptedMessage: encryptedMessage.toString("hex"), tag: tag.toString("hex"), salt: salt.toString("hex") // 盐需随加密结果传给前端 }; }; export default encryptMessage;
前端修复代码
const decryptMessage = async (encryptedData, password) => { const decoder = new TextDecoder(); // 封装hex转Uint8Array工具函数 const hexToUint8Array = (hexStr) => new Uint8Array(hexStr.match(/.{1,2}/g).map(byte => parseInt(byte, 16))); const iv = hexToUint8Array(encryptedData.iv); const encryptedMessage = hexToUint8Array(encryptedData.encryptedMessage); const tag = hexToUint8Array(encryptedData.tag); const salt = hexToUint8Array(encryptedData.salt); try { // 导入密码作为密钥材料 const keyMaterial = await crypto.subtle.importKey( "raw", new TextEncoder().encode(password), { name: "PBKDF2" }, false, ["deriveKey"] ); // 派生和后端一致的32字节AES-256密钥 const key = await crypto.subtle.deriveKey( { name: "PBKDF2", salt: salt, iterations: 100000, hash: "SHA-256" }, keyMaterial, { name: "AES-GCM", length: 256 }, false, ["decrypt"] ); // 执行解密 const decryptedBuffer = await crypto.subtle.decrypt( { name: "AES-GCM", iv: iv, tag: tag }, key, encryptedMessage ); const decryptedMessage = decoder.decode(decryptedBuffer); console.log("解密成功:", decryptedMessage); return decryptedMessage; } catch (error) { // 打印完整错误信息便于排查 console.error("解密错误详情:", error); throw new Error(`解密失败:${error.message}`); } } export default decryptMessage;
调用示例
// 后端加密 const encryptedData = encryptMessage("需要加密的敏感内容", "用户设置的密码"); // 前端解密(encryptedData为后端返回的对象) const decryptedContent = await decryptMessage(encryptedData, "用户输入的密码");
额外注意事项
- 盐的使用:每次加密必须生成随机盐,避免彩虹表攻击,盐需和加密结果一起传递给前端。
- 迭代次数:PBKDF2的迭代次数建议设置为10万次以上,平衡安全性与性能。
- 错误排查:前端catch块中务必打印完整的
error对象,可获取诸如密钥长度不符、tag验证失败等具体错误信息。
内容的提问来源于stack exchange,提问作者MD DANISH
相关产品推荐
相关产品推荐

