You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android应用重启后AES解密失败,报BadPaddingException错误

Android AES加密重启后解密报BadPaddingException问题修复

问题场景

我是Android开发初学者,实现图片AES加密功能:通过相机Intent捕获图片,加密后存储到文件系统。首次启动应用时加密解密流程正常,解密后的图片可正常显示;但杀死应用重启后,读取存储的加密图片解密时出现javax.crypto.BadPaddingException: pad block corrupted错误,图片无法显示。

核心问题原因

  1. 密钥与IV未持久化:加密使用的密钥和IV是静态变量,应用重启后这些变量会被重置,解密时使用的密钥/IV和加密时不一致,导致解密失败。
  2. 密钥生成逻辑错误:initializeEncryption()方法中,先调用keyGenerator.generateKey()生成随机密钥,之后才用指定的seed初始化keyGenerator,导致实际使用的密钥并非从你提供的key字符串派生而来。
  3. 加密模式与初始化混乱:
    • Cipher.getInstance("AES")默认使用ECB模式(无IV),但代码中却创建了IvParameterSpec,逻辑矛盾。
    • 加密方法中重复调用cipher.init(),第二次调用覆盖了第一次的IV设置,完全未使用IV。
  4. 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);
}

注意事项

  1. 盐的选择:SALT建议使用随机生成的固定值,不要硬编码在代码中(可以首次启动时生成并存储到SharedPreferences),避免反编译后泄露。
  2. Android Keystore:生产环境中,推荐将密钥存储到Android Keystore中,而不是从字符串派生,进一步提高安全性。
  3. GCM模式优势:GCM不需要padding,避免了BadPaddingException的常见场景,同时提供数据完整性校验,能检测密文是否被篡改。

内容的提问来源于stack exchange,提问作者chutwik

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 14:45:33