AES-GCM浏览器文件加密异常:任意密码均可解密,求排查
浏览器端AES-GCM文件加解密问题:任意密码可解密
我在使用AES-GCM算法实现浏览器端文件加解密时遇到问题,目前文件能完成加解密操作,但加密后的文件可以被任意密码解密,即使密码和加密时不符。以下是我的实现代码:
var FileSaver = require("file-saver"); export function formatBytes(bytes) { var marker = 1024; // Change to 1000 if required var decimal = 3; // Change as required var kiloBytes = marker; // One Kilobyte is 1024 bytes var megaBytes = marker * marker; // One MB is 1024 KB var gigaBytes = marker * marker * marker; // One GB is 1024 MB // return bytes if less than a KB if (bytes < kiloBytes) return bytes + " Bytes"; // return KB if less than a MB else if (bytes < megaBytes) return (bytes / kiloBytes).toFixed(decimal) + " KB"; // return MB if less than a GB else if (bytes < gigaBytes) return (bytes / megaBytes).toFixed(decimal) + " MB"; // return GB if less than a TB else return (bytes / gigaBytes).toFixed(decimal) + " GB"; } export const encryptFile = async (key, iv, file) => { return await window.crypto.subtle.encrypt( { name: "AES-GCM", iv: iv }, key, file ); }; /* Get some key material to use as input to the deriveKey method. The key material is a password supplied by the user. */ export const getKeyMaterial = async (password) => { let enc = new TextEncoder(); return window.crypto.subtle.importKey( "raw", enc.encode(password), { name: "PBKDF2" }, false, ["deriveBits", "deriveKey"] ); }; /* Given some key material and some random salt derive an AES-GCM key using PBKDF2. */ export const getKey = async (keyMaterial, salt) => { return window.crypto.subtle.deriveKey( { name: "PBKDF2", salt: salt, iterations: 100000, hash: "SHA-256" }, keyMaterial, { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"] ); }; // get the iv which is similar the salt value used for encryption export const getiv = () => { return window.crypto.getRandomValues(new Uint8Array(12)); }; // load the file in the memory export const getFile = async (inputFile) => { return await inputFile.arrayBuffer(); }; export const decryptFile = (iv, key, cipherText) => { return window.crypto.subtle.decrypt( { name: "AES-GCM", iv: iv }, key, cipherText ); }; export const getDigest = (uid) => { let enc = new TextEncoder(); return crypto.subtle.digest("SHA-256", enc.encode(uid)); }; export const startEncryption = async (file, password) => { console.log(file, password); let digest = getDigest(password); let rawFile = getFile(file); let keyMaterial = getKeyMaterial(); Promise.all([digest, rawFile, keyMaterial]).then((values) => { console.log(values); let salt = new Uint8Array( "12345678".match(/[\da-f]{2}/gi).map(function (h) { return parseInt(h, 16); }) ); let iv = new Uint8Array( "4142434445464748494a4b4c4d4e4f50" .match(/[\da-f]{2}/gi) .map(function (h) { return parseInt(h, 16); }) ); // generate a crypto key getKey(values[2], salt).then((resp) => { console.log(resp); encryptFile(resp, iv, values[1]).then((cipherText) => { let fileBlob = new Blob([cipherText], { type: file.type }); FileSaver.saveAs(fileBlob, file.name); }); }); }); }; export const startDecryption = (cipherText, password) => { let keyMaterial = getKeyMaterial(); let digest = getDigest(password); let rawFile = getFile(cipherText); Promise.all([digest, rawFile, keyMaterial]).then((values) => { let salt = new Uint8Array( "12345678".match(/[\da-f]{2}/gi).map(function (h) { return parseInt(h, 16); }) ); let iv = new Uint8Array( "4142434445464748494a4b4c4d4e4f50" .match(/[\da-f]{2}/gi) .map(function (h) { return parseInt(h, 16); }) ); // generate a crypto key getKey(values[2], salt).then((resp) => { decryptFile(iv, resp, values[1]).then((file) => { let fileBlob = new Blob([file], { type: file.type }); FileSaver.saveAs(fileBlob, file.name); }); }); }); };
核心问题分析
密钥材料生成参数缺失
在startEncryption和startDecryption中,调用getKeyMaterial()时没有传入password参数,导致始终用空字符串生成密钥材料。不管输入什么密码,最终派生的密钥完全一致,自然能解密任意文件。硬编码盐值与IV
代码中盐值(salt)和IV都写死为固定值,不符合密码学最佳实践:每次加密必须使用随机盐值和IV,且盐值需要和密文一起存储,解密时才能用相同盐值派生对应密钥。未处理AES-GCM认证标签
AES-GCM是带认证的加密算法,加密后会生成认证标签用于验证数据完整性,但你的代码没有将标签与密文绑定存储,解密时也未验证标签,不仅存在安全隐患,也无法触发密码错误的异常。
修复后的代码实现
1. 修复核心参数问题
确保调用getKeyMaterial时传入用户密码,同时改用async/await简化异步逻辑:
// 加密流程修复 export const startEncryption = async (file, password) => { const rawFile = await getFile(file); const keyMaterial = await getKeyMaterial(password); // 生成随机盐值(推荐16字节) const salt = window.crypto.getRandomValues(new Uint8Array(16)); // 生成随机IV(AES-GCM推荐12字节) const iv = window.crypto.getRandomValues(new Uint8Array(12)); const key = await getKey(keyMaterial, salt); // AES-GCM加密结果包含密文+认证标签 const encrypted = await window.crypto.subtle.encrypt( { name: "AES-GCM", iv: iv }, key, rawFile ); // 组装数据:盐值(16) + IV(12) + 密文+标签 const combined = new Uint8Array(salt.length + iv.length + encrypted.byteLength); combined.set(salt, 0); combined.set(iv, salt.length); combined.set(new Uint8Array(encrypted), salt.length + iv.length); // 保存加密文件,添加后缀区分 const fileBlob = new Blob([combined], { type: file.type }); FileSaver.saveAs(fileBlob, `${file.name}.encrypted`); }; // 解密流程修复 export const startDecryption = async (encryptedFile, password) => { const rawCombined = await getFile(encryptedFile); const keyMaterial = await getKeyMaterial(password); // 拆分存储的数据:前16字节盐值,接下来12字节IV,剩余是密文+标签 const salt = new Uint8Array(rawCombined.slice(0, 16)); const iv = new Uint8Array(rawCombined.slice(16, 28)); const ciphertextWithTag = rawCombined.slice(28); const key = await getKey(keyMaterial, salt); try { // 解密时自动验证标签,密码错误会抛出异常 const decrypted = await window.crypto.subtle.decrypt( { name: "AES-GCM", iv: iv }, key, ciphertextWithTag ); // 还原原始文件名 const originalName = encryptedFile.name.replace('.encrypted', ''); const fileBlob = new Blob([decrypted], { type: encryptedFile.type }); FileSaver.saveAs(fileBlob, originalName); } catch (e) { alert('密码错误或文件已被篡改'); console.error('解密失败:', e); } };
2. 其他优化
- 移除无用的
getDigest函数; - 解密时增加异常捕获,密码错误或文件篡改时给出明确提示;
- 加密文件添加
.encrypted后缀,方便解密时识别。
内容的提问来源于stack exchange,提问作者Lunatix01
相关产品推荐
相关产品推荐

