AES/CBC/PKCS5Padding加密内容重启应用后无法解密的技术求助
问题诊断与解决方案
嘿,我一眼就看出问题出在哪了——根本不是密钥比特长度的问题,核心原因是你没有保存加密时使用的IV向量!
问题根源解释
AES-CBC模式的加密和解密必须使用**完全相同的IV(初始化向量)**才能正常工作。你当前的代码里,每次调用generateIv()都会生成一个全新的随机IV,加密后只把密文转成Base64存储,却没有把这个IV保存下来。当应用重启后,你又生成了一个新的IV去解密旧密文,这必然导致解密失败,因为CBC模式的每一块加密都依赖前一块的结果,而IV是第一块的“初始前块”,不匹配的话整个解密流程都会出错。
至于你担心的密钥长度问题:你用PBKDF2生成的是256位AES密钥,只要密码("pass")和盐("salt")不变,每次重启应用生成的密钥都是完全一致的,这部分没有问题。
修复方案
解决思路很简单:把加密时使用的IV和密文一起存储,解密时先还原出这个IV,再用它来初始化解密模式的Cipher。
常见的做法是把IV转成Base64字符串,和密文用一个不会在Base64中出现的分隔符(比如冒号:)拼接起来,或者分开存储到两个字段里。下面是修改后的代码示例:
import javax.crypto.*; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.security.spec.KeySpec; import java.util.Base64; import java.util.Random; public class AESExample { private static final String UTF_8 = StandardCharsets.UTF_8.name(); // 用于分隔IV和密文的分隔符,Base64中不会出现冒号,所以很安全 private static final String IV_SEPARATOR = ":"; private static SecretKey getKeyFromPassword(String password, String salt) throws NoSuchAlgorithmException, InvalidKeySpecException { SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(StandardCharsets.UTF_8), 65536, 256); SecretKey secret = new SecretKeySpec(factory.generateSecret(spec) .getEncoded(), "AES"); return secret; } private static IvParameterSpec generateIv() { byte[] iv = new byte[16]; new Random().nextBytes(iv); return new IvParameterSpec(iv); } // 加密方法:返回IV:密文的Base64字符串 public static String encrypt(String plainText, SecretKey key, IvParameterSpec iv) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, key, iv); byte[] cipherText = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); // 把IV转成Base64,和密文Base64拼接 String ivBase64 = Base64.getEncoder().encodeToString(iv.getIV()); String cipherTextBase64 = Base64.getEncoder().encodeToString(cipherText); return ivBase64 + IV_SEPARATOR + cipherTextBase64; } // 解密方法:从IV:密文中还原出明文 public static String decrypt(String encryptedText, SecretKey key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { // 拆分IV和密文 String[] parts = encryptedText.split(IV_SEPARATOR); String ivBase64 = parts[0]; String cipherTextBase64 = parts[1]; // 还原IV和密文字节数组 byte[] iv = Base64.getDecoder().decode(ivBase64); byte[] cipherText = Base64.getDecoder().decode(cipherTextBase64); // 初始化解密模式的Cipher Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv)); byte[] plainText = cipher.doFinal(cipherText); return new String(plainText, StandardCharsets.UTF_8); } public static void main(String[] args) throws Exception { // 测试文本 String teste = "teste teste teste teste"; // 生成密钥(密码和盐固定,重启后生成的密钥一致) SecretKey secretKey = getKeyFromPassword("pass", "salt"); // 生成IV IvParameterSpec ivParameterSpec = generateIv(); // 加密并存储 String encrypted = encrypt(teste, secretKey, ivParameterSpec); System.out.println("加密后存储的内容:" + encrypted); // 模拟应用重启:重新生成密钥(和之前一致),然后解密 SecretKey restartedKey = getKeyFromPassword("pass", "salt"); String decrypted = decrypt(encrypted, restartedKey); System.out.println("解密后的文本:" + decrypted); } }
额外注意事项
- IV不需要保密,它可以和密文一起公开存储,唯一要求是每次加密都使用随机的IV(你当前的
generateIv()已经做到了这一点,这很重要,能保证相同明文加密后得到不同的密文,提升安全性)。 - 确保分隔符不会出现在Base64字符串中,Base64的字符集是A-Z、a-z、0-9、+、/,所以用冒号
:、分号;等都是安全的。
内容的提问来源于stack exchange,提问作者Myphre
相关产品推荐
相关产品推荐

