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
相关产品推荐
相关产品推荐

