You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
      });
    });
  });
};

核心问题分析

  1. 密钥材料生成参数缺失
    在startEncryption和startDecryption中,调用getKeyMaterial()时没有传入password参数,导致始终用空字符串生成密钥材料。不管输入什么密码,最终派生的密钥完全一致,自然能解密任意文件。

  2. 硬编码盐值与IV
    代码中盐值(salt)和IV都写死为固定值,不符合密码学最佳实践:每次加密必须使用随机盐值和IV,且盐值需要和密文一起存储,解密时才能用相同盐值派生对应密钥。

  3. 未处理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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 22:55:19