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

AES/CBC/PKCS5Padding加密Java转Node.js解密报bad decrypt错误

Java AES/CBC/PKCS5Padding 转Node.js 解密失败问题修复

问题背景

我正在尝试将基于AES/CBC/PKCS5Padding算法实现加解密的Java代码转换为Node.js代码,由于自定义密文格式封装了动态盐、IV与加密内容,实现过程存在解密报错问题,Java代码main方法中提供了可运行的测试示例。

Java侧配置与密文规则

  • 动态盐长度:10字节
  • 密钥派生规则:使用PBKDF2WithHmacSHA1算法,迭代次数65556,派生AES密钥长度256位
  • 加密逻辑:先生成10字节随机动态盐,通过用户密码+固定盐派生AES密钥,加密时自动生成CBC模式所需IV,最终密文结构为动态盐 + IV + PKCS5Padding加密密文,整体经Base64编码后返回
  • 解密逻辑:先对密文做Base64解码,跳过前10位动态盐,读取后续16字节作为IV,剩余字节为实际密文,使用派生密钥完成解密

参考Java实现代码

public class App {
    private static final int DYN_SALT_LENGTH = 10;
    private static final int ITERATION_COUNT = 65556;
    private static final int KEY_LENGTH = 256;
    private static final String SECRET_KEY_ALGORITHM = "AES";
    private static final String CIPHER_TRANSFORMER = "AES/CBC/PKCS5Padding";
    private static Base64 base64Instance = new Base64();

    public static String decrypt(String data, String password, String salt) {
        try {
            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
            PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(StandardCharsets.UTF_8),
                    ITERATION_COUNT,
                    KEY_LENGTH);

            SecretKey secretKey = factory.generateSecret(spec);

            ByteBuffer buffer = ByteBuffer.wrap(base64Instance.decode(data));
            buffer.position(DYN_SALT_LENGTH);
            Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMER);

            // Read the IV
            byte[] ivBytes = new byte[cipher.getBlockSize()];
            buffer.get(ivBytes, 0, ivBytes.length);

            // Read encrypted text.
            byte[] encryptedTextBytes = new byte[buffer.capacity() - DYN_SALT_LENGTH - ivBytes.length];
            buffer.get(encryptedTextBytes);

            // Initialize Cipher.
            SecretKeySpec secret = new SecretKeySpec(secretKey.getEncoded(), SECRET_KEY_ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(ivBytes));

            String result = new String(cipher.doFinal(encryptedTextBytes), StandardCharsets.UTF_8);
            return result;

        } catch (Exception e) {
            throw new RuntimeException("Failed to decrypt data", e);
        }
    }

    public static String encrypt(String data, String password, String salt) {
        // Create new salt for every new encryption request.
        byte[] saltBytes = new byte[DYN_SALT_LENGTH];
        new SecureRandom().nextBytes(saltBytes);

        try {
            // Create secret key spec.
            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
            PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(StandardCharsets.UTF_8),
                    ITERATION_COUNT,
                    KEY_LENGTH);
            SecretKey secretKey = factory.generateSecret(spec);
            SecretKeySpec secret = new SecretKeySpec(secretKey.getEncoded(), SECRET_KEY_ALGORITHM);
            byte[] ivBytes;
            byte[] encryptedTextBytes;

            // Initialize cipher
            Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMER);
            cipher.init(Cipher.ENCRYPT_MODE, secret);

            AlgorithmParameters params = cipher.getParameters();

            // Create initialization vector IV
            ivBytes = params.getParameterSpec(IvParameterSpec.class).getIV();

            // Encrypt the text.
            encryptedTextBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));

            // Response will be in the form of <salt><IV><encryptedText>
            ByteBuffer byteBuffer = ByteBuffer.allocate(saltBytes.length + ivBytes.length + encryptedTextBytes.length);
            byteBuffer.put(saltBytes);
            byteBuffer.put(ivBytes);
            byteBuffer.put(encryptedTextBytes);

            return base64Instance.encodeToString(byteBuffer.array());
        } catch (Exception e) {
            throw new RuntimeException("Failed to encrypt data", e);
        }

    }

    public static void main(String[] args) {
        String password = "password";
        String salt = "salt";
        String data = "hello world";

        String resultEncrypted = encrypt(data, password, salt);
        System.out.println(resultEncrypted);
        String resultDecrypted = decrypt(resultEncrypted, password, salt);
        System.out.println(resultDecrypted);
    }
}

原有Node.js实现与报错

原有Node.js解密代码如下:

function getAlgorithm(keyBase64) {
  var key = Buffer.from(keyBase64, "base64");
  switch (key.length) {
    case 16:
      return "aes-128-cbc";
    case 32:
      return "aes-256-cbc";
  }

  throw new Error("Invalid key length: " + key.length);
}

function decrypt(messagebase64, keyBase64, ivBase64) {
  const key = Buffer.from(keyBase64, "base64");
  const iv = Buffer.from(ivBase64, "base64");

  const decipher = crypto.createDecipheriv(
    getAlgorithm(keyBase64),
    key,
    iv.slice(0, 16)
  );
  let decrypted = decipher.update(messagebase64, "base64", "utf8");
  decrypted += decipher.final("utf8");
  return decrypted;
}

const base64Encrypted =
  "2vSIh0J64zhrQuayUV+UIyPTpmSaN4gAv7B3CVC/a68eBfeU0bMwRm2I";

const key = crypto.scryptSync("password", "salt", 16);

const encrypted = Buffer.from(base64Encrypted, "base64");
const encryptedWOSalt = Buffer.from(base64Encrypted, "base64").slice(10);
const iv = encrypted.slice(10, 10 + 17);

const result = decrypt(
  encryptedWOSalt.toString("base64"),
  key,
  iv.toString("base64")
);
console.log(result);

运行时抛出错误:

Error: error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt

错误原因

  • 密钥派生逻辑错误:Java使用PBKDF2WithHmacSHA1派生32字节(256位)密钥,原代码错误使用scryptSync派生16字节密钥,算法、密钥长度均和Java侧不匹配
  • IV截取长度错误:AES CBC模式IV固定为16字节(等于AES块大小),原代码截取IV时偏移量错误取到17字节,IV值非法
  • 密文拆分错误:原代码将去掉动态盐后的全部内容(包含IV)直接作为密文传入解密函数,没有拆分出IV和实际加密内容,传入解密的密文数据错误
  • 多余的Base64编解码:密文拆分后不需要重复转Base64,直接传入Buffer即可,重复转码会导致密文数据损坏

修复后可运行代码

const crypto = require('crypto');

// 配置和Java侧完全对齐
const DYN_SALT_LENGTH = 10;
const ITERATION_COUNT = 65556;
const KEY_LENGTH = 32; // 256位密钥对应32字节
const IV_LENGTH = 16; // AES块大小固定16字节

function decrypt(encryptedBase64, password, fixedSalt) {
  // Base64解码完整密文
  const encryptedBuffer = Buffer.from(encryptedBase64, 'base64');
  // 按规则拆分动态盐、IV、实际密文,动态盐解密不需要使用直接跳过
  const iv = encryptedBuffer.subarray(DYN_SALT_LENGTH, DYN_SALT_LENGTH + IV_LENGTH);
  const cipherText = encryptedBuffer.subarray(DYN_SALT_LENGTH + IV_LENGTH);
  // 使用PBKDF2WithHmacSHA1派生密钥,参数和Java完全一致
  const key = crypto.pbkdf2Sync(
    password,
    Buffer.from(fixedSalt, 'utf8'),
    ITERATION_COUNT,
    KEY_LENGTH,
    'sha1'
  );
  // 初始化解密器,aes-256-cbc默认使用PKCS7填充,和Java的PKCS5Padding完全兼容
  const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
  // 执行解密
  let decrypted = decipher.update(cipherText, null, 'utf8');
  decrypted += decipher.final('utf8');
  return decrypted;
}

// 测试用例
const base64Encrypted = "2vSIh0J64zhrQuayUV+UIyPTpmSaN4gAv7B3CVC/a68eBfeU0bMwRm2I";
const password = "password";
const fixedSalt = "salt";
console.log(decrypt(base64Encrypted, password, fixedSalt)); // 输出 hello world

内容的提问来源于stack exchange,提问作者Jonnatan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 04:57:19