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

后端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);

错误原因分析

  1. 密钥长度不匹配:AES-256要求密钥必须是32字节(256位)。后端代码直接将credential转成Buffer作为密钥,若credential的UTF-8编码长度不是32字节,Node.js的crypto模块会自动截断为32字节;但前端Web Crypto API会严格校验密钥长度,长度不符直接触发解密失败。
  2. 错误信息被掩盖:前端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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 16:43:18