Android应用重启后AES解密失败,报BadPaddingException错误
Android AES加密重启后解密报BadPaddingException问题修复
问题场景
我是Android开发初学者,实现图片AES加密功能:通过相机Intent捕获图片,加密后存储到文件系统。首次启动应用时加密解密流程正常,解密后的图片可正常显示;但杀死应用重启后,读取存储的加密图片解密时出现javax.crypto.BadPaddingException: pad block corrupted错误,图片无法显示。
核心问题原因
- 密钥与IV未持久化:加密使用的密钥和IV是静态变量,应用重启后这些变量会被重置,解密时使用的密钥/IV和加密时不一致,导致解密失败。
- 密钥生成逻辑错误:
initializeEncryption()方法中,先调用keyGenerator.generateKey()生成随机密钥,之后才用指定的seed初始化keyGenerator,导致实际使用的密钥并非从你提供的key字符串派生而来。 - 加密模式与初始化混乱:
Cipher.getInstance("AES")默认使用ECB模式(无IV),但代码中却创建了IvParameterSpec,逻辑矛盾。- 加密方法中重复调用
cipher.init(),第二次调用覆盖了第一次的IV设置,完全未使用IV。
- IV未与密文一起存储:即使生成了IV,也没有将其和加密后的图片数据一起保存,重启后无法获取正确的IV用于解密。
修复方案
1. 使用安全的AES模式(推荐GCM)
GCM模式自带认证,能同时保证加密安全性和数据完整性,避免padding相关问题。如果坚持用CBC模式,必须配合随机IV并存储IV。
2. 从固定字符串派生密钥(PBKDF2)
使用PBKDF2算法从你提供的key字符串派生AES密钥,确保每次启动应用都能生成相同的密钥(只要输入的字符串和盐一致)。
3. 生成随机IV并与密文一起存储
每次加密生成随机IV,将IV放在密文的开头(或单独存储),解密时先读取IV再解密。
4. 避免使用静态变量存储密钥/IV
改用局部变量或安全的存储方式(如Android Keystore,初学者可先文件存储IV,密钥从字符串派生)。
修改后的完整代码
加密解密工具类
import android.util.Base64; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; import java.security.spec.KeySpec; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; public class LocalFilesEncryptionAndDecryptionMechanism { // 固定参数:盐、迭代次数、密钥长度 private static final String SALT = "your_custom_salt_here"; // 替换为自己的随机盐,建议16字节以上 private static final int ITERATION_COUNT = 65536; private static final int KEY_LENGTH = 256; private static final String AES_MODE = "AES/GCM/NoPadding"; private static final int GCM_TAG_LENGTH = 128; // 128位完整性标签 private static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA256"; private static final String KEY_STRING = "lzk9rcgxnH8Ah2p1iL7LTB7DE9/N3+rQqMaPCwfRyoxzoWGECiy6E2euV/6qnQ1AVrMpJStDFDd1jJsvwxvhFG2tkJOjtWr3dYANDc/bEWhKL5YI6kKz6pSclJvnN8sO"; // 从字符串派生AES密钥 private static SecretKey getAesKey() throws NoSuchAlgorithmException, InvalidKeySpecException { SecretKeyFactory factory = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM); KeySpec spec = new PBEKeySpec(KEY_STRING.toCharArray(), SALT.getBytes(StandardCharsets.UTF_8), ITERATION_COUNT, KEY_LENGTH); SecretKey tempKey = factory.generateSecret(spec); return new SecretKeySpec(tempKey.getEncoded(), "AES"); } // 加密:返回IV+密文的组合数组(IV前12字节) public static byte[] encrypt(byte[] plaintext) throws Exception { SecureRandom random = new SecureRandom(); byte[] iv = new byte[12]; // GCM推荐使用12字节IV random.nextBytes(iv); Cipher cipher = Cipher.getInstance(AES_MODE); GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv); cipher.init(Cipher.ENCRYPT_MODE, getAesKey(), spec); byte[] ciphertext = cipher.doFinal(plaintext); // 将IV和密文合并:IV在前,密文在后 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); outputStream.write(iv); outputStream.write(ciphertext); return outputStream.toByteArray(); } // 解密:从组合数组中分离IV和密文 public static byte[] decrypt(byte[] encryptedData) throws Exception { // 分离IV和密文 byte[] iv = new byte[12]; byte[] ciphertext = new byte[encryptedData.length - 12]; System.arraycopy(encryptedData, 0, iv, 0, 12); System.arraycopy(encryptedData, 12, ciphertext, 0, ciphertext.length); Cipher cipher = Cipher.getInstance(AES_MODE); GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv); cipher.init(Cipher.DECRYPT_MODE, getAesKey(), spec); return cipher.doFinal(ciphertext); } public static byte[] readAllBytes(FileInputStream inputStream) throws IOException { final int bufLen = 4 * 0x400; // 4KB byte[] buf = new byte[bufLen]; int readLen; IOException exception = null; try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { while ((readLen = inputStream.read(buf, 0, bufLen)) != -1) { outputStream.write(buf, 0, readLen); } return outputStream.toByteArray(); } catch (IOException e) { exception = e; throw e; } finally { if (exception == null) { inputStream.close(); } else { try { inputStream.close(); } catch (IOException e) { exception.addSuppressed(e); } } } } }
图片保存代码修改
private String saveToInternalStorage(Bitmap bitmapImage, String rootFolder, String cameraPictureFolderName, String imgName) { final File directory = new File(getActivity().getFilesDir() + File.separator + rootFolder + File.separator + cameraPictureFolderName); if (!directory.exists()) { directory.mkdirs(); } File mypath = new File(directory, imgName); try (ByteArrayOutputStream fos = new ByteArrayOutputStream(); FileOutputStream imageOutputStream = new FileOutputStream(mypath)) { // 压缩图片为PNG bitmapImage.compress(Bitmap.CompressFormat.PNG, 100, fos); // 加密图片字节 byte[] encryptedImage = LocalFilesEncryptionAndDecryptionMechanism.encrypt(fos.toByteArray()); // 写入文件 imageOutputStream.write(encryptedImage); imageOutputStream.flush(); } catch (Exception e) { e.printStackTrace(); } return mypath.getAbsolutePath(); } // 相机捕获后的保存逻辑 String rootFolder = getString(R.string.thumbnail_images); String folderPath = rootFolder + "/" + cameraPictureFolderName; File week_Folder = getActivity().getExternalFilesDir(folderPath); String filePath = week_Folder.getPath() + "/" + cameraPictureFileName; File final_file = new File(filePath); try (FileOutputStream fo = new FileOutputStream(final_file)) { saveToInternalStorage(BM, rootFolder, cameraPictureFolderName, cameraPictureFileName); byte[] encryptedBytes = LocalFilesEncryptionAndDecryptionMechanism.encrypt(bytes.toByteArray()); fo.write(encryptedBytes); fo.flush(); } catch (Exception e) { e.printStackTrace(); }
解密加载图片代码
File imageFile = new File(context.getFilesDir() + "/" + folderPath + "/" + imageNameArray[position]); Bitmap b = null; try (FileInputStream fileInputStream = new FileInputStream(imageFile)) { byte[] encryptedBytes = LocalFilesEncryptionAndDecryptionMechanism.readAllBytes(fileInputStream); byte[] decryptedBytes = LocalFilesEncryptionAndDecryptionMechanism.decrypt(encryptedBytes); b = BitmapFactory.decodeByteArray(decryptedBytes, 0, decryptedBytes.length); } catch (Exception e) { e.printStackTrace(); } if (b != null) { imageItem.setImageBitmap(b); }
注意事项
- 盐的选择:
SALT建议使用随机生成的固定值,不要硬编码在代码中(可以首次启动时生成并存储到SharedPreferences),避免反编译后泄露。 - Android Keystore:生产环境中,推荐将密钥存储到Android Keystore中,而不是从字符串派生,进一步提高安全性。
- GCM模式优势:GCM不需要padding,避免了
BadPaddingException的常见场景,同时提供数据完整性校验,能检测密文是否被篡改。
内容的提问来源于stack exchange,提问作者chutwik
相关产品推荐
相关产品推荐

