如何修复javax.crypto.BadPaddingException: 给定最终块未正确填充
修复AES解密时的BadPaddingException异常
问题背景
我用Java实现密码保险箱,编写了EncryptionModule类,包含encrypt(String password, String databaseName)和decrypt(String password, String databaseName)方法,支持CSV文件加解密。执行解密时抛出异常:
Caused by: javax.crypto.BadPaddingException: Given final block not properly padded. Such issues can arise if a bad key is used during decryption.
此前曾遇到java.security.InvalidKeyException: Invalid AES key length异常,修改代码后出现当前问题。
核心问题分析
当前代码的致命错误是解密流程错误地重新生成了salt和IV:
- 加密时,代码生成随机的salt和IV,将
IV(16字节) + salt(16字节) + 密文写入加密文件; - 但解密时,代码没有从加密文件中读取已存储的salt和IV,而是重新生成了新的随机值,导致用错误的参数推导密钥,最终解密失败抛出BadPaddingException。
修复方案
拆分加密和解密逻辑,确保解密时使用加密过程中生成并存储的salt和IV:
修改后的EncryptionModule代码
package com.example.passwordsafe.data; import com.example.passwordsafe.core.usecases.EncryptionModuleInterface; import javax.crypto.*; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.security.*; import java.security.spec.InvalidKeySpecException; import java.security.spec.KeySpec; public class EncryptionModule implements EncryptionModuleInterface { private static final String PBKDF_ALGORITHM = "PBKDF2WithHmacSHA1"; private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding"; private static final String ALGORITHM = "AES"; private static final int SALT_LENGTH = 16; private static final int IV_LENGTH = 16; private static final int ITERATION_COUNT = 1000000; private static final int KEY_LENGTH = 256; @Override public void encrypt(String password, String databaseName) { Path path = Paths.get(databaseName); if (Files.exists(path)) { File plaintextFile = new File(databaseName); File encryptedFile = new File(databaseName + ".encrypted"); doEncrypt(password, plaintextFile, encryptedFile); } else { System.out.println("File does not exist"); } } @Override public void decrypt(String password, String databaseName) { Path path = Paths.get(databaseName); if (Files.exists(path)) { File encryptedFile = new File(databaseName); File plaintextFile = new File(databaseName.replace(".encrypted", ".decrypted")); doDecrypt(password, encryptedFile, plaintextFile); } else { System.out.println("File does not exist"); } } private void doEncrypt(String password, File inputFile, File outputFile) { SecureRandom random = new SecureRandom(); byte[] salt = new byte[SALT_LENGTH]; random.nextBytes(salt); byte[] ivBytes = new byte[IV_LENGTH]; random.nextBytes(ivBytes); try { // 推导密钥 SecretKeySpec keySpec = deriveKey(password, salt); IvParameterSpec iv = new IvParameterSpec(ivBytes); // 初始化Cipher Cipher cipher = Cipher.getInstance(TRANSFORMATION); cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv); // 读取明文并加密 FileInputStream inputStream = new FileInputStream(inputFile); byte[] inputBytes = new byte[(int) inputFile.length()]; inputStream.read(inputBytes); byte[] encValue = cipher.doFinal(inputBytes); // 组装输出数据:IV + salt + 密文 byte[] finalOutput = new byte[IV_LENGTH + SALT_LENGTH + encValue.length]; System.arraycopy(ivBytes, 0, finalOutput, 0, IV_LENGTH); System.arraycopy(salt, 0, finalOutput, IV_LENGTH, SALT_LENGTH); System.arraycopy(encValue, 0, finalOutput, IV_LENGTH + SALT_LENGTH, encValue.length); // 写入加密文件 FileOutputStream outputStream = new FileOutputStream(outputFile); outputStream.write(finalOutput); inputStream.close(); outputStream.close(); } catch (Exception e) { throw new RuntimeException(e); } } private void doDecrypt(String password, File inputFile, File outputFile) { try { FileInputStream inputStream = new FileInputStream(inputFile); byte[] inputBytes = new byte[(int) inputFile.length()]; inputStream.read(inputBytes); // 从加密文件中读取IV和salt byte[] ivBytes = new byte[IV_LENGTH]; System.arraycopy(inputBytes, 0, ivBytes, 0, IV_LENGTH); byte[] salt = new byte[SALT_LENGTH]; System.arraycopy(inputBytes, IV_LENGTH, salt, 0, SALT_LENGTH); // 剩余部分是密文 byte[] encValue = new byte[inputBytes.length - IV_LENGTH - SALT_LENGTH]; System.arraycopy(inputBytes, IV_LENGTH + SALT_LENGTH, encValue, 0, encValue.length); // 推导密钥(使用加密时的salt) SecretKeySpec keySpec = deriveKey(password, salt); IvParameterSpec iv = new IvParameterSpec(ivBytes); // 初始化Cipher并解密 Cipher cipher = Cipher.getInstance(TRANSFORMATION); cipher.init(Cipher.DECRYPT_MODE, keySpec, iv); byte[] decValue = cipher.doFinal(encValue); // 写入明文文件 FileOutputStream outputStream = new FileOutputStream(outputFile); outputStream.write(decValue); inputStream.close(); outputStream.close(); } catch (Exception e) { throw new RuntimeException(e); } } // 提取密钥推导逻辑为公共方法,确保加密解密使用相同逻辑 private SecretKeySpec deriveKey(String password, byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException { KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATION_COUNT, KEY_LENGTH); SecretKeyFactory factory = SecretKeyFactory.getInstance(PBKDF_ALGORITHM); byte[] key = factory.generateSecret(spec).getEncoded(); return new SecretKeySpec(key, ALGORITHM); } }
额外优化点
- 将密钥推导逻辑提取为
deriveKey方法,避免重复代码,确保加密解密使用完全一致的密钥生成逻辑; - 解密时调整输出文件名,用
replace(".encrypted", ".decrypted")替代直接拼接,避免生成passwords.csv.encrypted.decrypted这类冗余文件名; - 定义常量存储固定长度和迭代次数,提升代码可读性和可维护性。
内容的提问来源于stack exchange,提问作者max23
相关产品推荐
相关产品推荐

