AES加解密工具类执行失败,抛出BadPaddingException问题排查
问题描述
我实现了一个用于AES加解密字符串的工具类CryptoHelper,代码参考了AES加密示例。
工具类代码如下:
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.Security; import java.security.spec.InvalidKeySpecException; import java.security.spec.KeySpec; import java.util.Arrays; import java.util.Base64; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; public class CryptoHelper { private static final String CIPHER_ALGORITM_NAME = "AES/CBC/PKCS5Padding"; private static final String HASHING_ALGO_NAME = "PBKDF2WithHmacSHA1"; private static final int KEY_TARGET_LENGTH = 256; private static final int HASHING_ITERATIONS = 65536; public static SecretKey getSecretKey(String string, byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException { KeySpec spec = new PBEKeySpec(string.toCharArray(), salt, HASHING_ITERATIONS, KEY_TARGET_LENGTH); try { SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(HASHING_ALGO_NAME); SecretKey encryptedPassword = new SecretKeySpec(keyFactory.generateSecret(spec).getEncoded(), "AES"); return encryptedPassword; } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { throw e; } } public static IvParameterSpec getInitializationVector() { byte[] iv = new byte[16]; new SecureRandom().nextBytes(iv); return new IvParameterSpec(iv); } public static String encrypt(String input, String password, byte[] salt) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { Cipher cipher = Cipher.getInstance(CIPHER_ALGORITM_NAME); SecretKey secretKey = null; try { secretKey = getSecretKey(password, salt); } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { // code } cipher.init(Cipher.ENCRYPT_MODE, secretKey, getInitializationVector()); byte[] cipherText = cipher.doFinal(input.getBytes()); return Base64.getEncoder().encodeToString(cipherText); } public static String decrypt(String encryptedText, String password, byte[] salt) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { Cipher cipher = Cipher.getInstance(CIPHER_ALGORITM_NAME); SecretKey secretKey = null; try { secretKey = getSecretKey(password, salt); } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { // code } cipher.init(Cipher.DECRYPT_MODE, secretKey, getInitializationVector()); byte[] plainText = cipher.doFinal(Base64.getDecoder().decode(encryptedText)); return new String(plainText); } }
编写单元测试验证时,抛出了以下异常:
javax.crypto.BadPaddingException: Given final block not properly padded. Such issues can arise if a bad key is used during decryption.
测试代码如下:
@Test public void testDecrypt() { String encryptedString = ""; String password = "password"; try { encryptedString = CryptoHelper.encrypt("some string", password, password.getBytes()); } catch (InvalidKeyException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException | BadPaddingException | IllegalBlockSizeException e) { e.printStackTrace(); assertNull(e); } try { CryptoHelper.decrypt(encryptedString, password, password.getBytes()); } catch (InvalidKeyException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException | BadPaddingException | IllegalBlockSizeException e) { // TODO Auto-generated catch block e.printStackTrace(); } }
请问问题出在哪里?
问题原因及解决方案
核心问题:加密和解密使用了不同的初始化向量(IV)
你当前的代码中,encrypt和decrypt方法各自调用getInitializationVector()生成全新的随机IV。但AES/CBC模式要求加密和解密必须使用完全相同的IV,否则无法正确解密,就会抛出BadPaddingException。
IV本身不需要保密,但必须和密文一起保存,才能在解密时复用。
修复方案
修改工具类,将加密生成的IV和密文拼接在一起(比如先存IV的Base64编码,再存密文的Base64编码,用分隔符分开),解密时再拆分出IV和密文:
- 修改
encrypt方法,返回IV+密文的组合字符串:
public static String encrypt(String input, String password, byte[] salt) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, InvalidKeySpecException { Cipher cipher = Cipher.getInstance(CIPHER_ALGORITM_NAME); SecretKey secretKey = getSecretKey(password, salt); IvParameterSpec ivSpec = getInitializationVector(); cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); byte[] cipherText = cipher.doFinal(input.getBytes()); // 将IV和密文分别Base64编码后用冒号分隔 String ivBase64 = Base64.getEncoder().encodeToString(ivSpec.getIV()); String cipherBase64 = Base64.getEncoder().encodeToString(cipherText); return ivBase64 + ":" + cipherBase64; }
- 修改
decrypt方法,先拆分出IV和密文再解密:
public static String decrypt(String encryptedCombined, String password, byte[] salt) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, InvalidKeySpecException { // 拆分IV和密文 String[] parts = encryptedCombined.split(":", 2); if (parts.length != 2) { throw new IllegalArgumentException("无效的加密字符串格式"); } byte[] iv = Base64.getDecoder().decode(parts[0]); byte[] cipherText = Base64.getDecoder().decode(parts[1]); Cipher cipher = Cipher.getInstance(CIPHER_ALGORITM_NAME); SecretKey secretKey = getSecretKey(password, salt); IvParameterSpec ivSpec = new IvParameterSpec(iv); cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec); byte[] plainText = cipher.doFinal(cipherText); return new String(plainText); }
- 额外修复:原代码中
encrypt和decrypt方法里捕获getSecretKey的异常后直接忽略,会导致secretKey为null,后续初始化Cipher时也会报错,需要去掉这部分无效的异常捕获,让异常正常向上抛出。
额外优化点
- 不要用
password.getBytes()作为salt,salt应该随机生成,并且和IV、密文一起保存,否则会降低密钥推导的安全性。 - 可以考虑使用更安全的哈希算法,比如
PBKDF2WithHmacSHA256替代PBKDF2WithHmacSHA1。
内容的提问来源于stack exchange,提问作者WolfiG
相关产品推荐
相关产品推荐

