大文件加密解密异常:解密时抛出IllegalBlockSizeException
解决AES解密时
IllegalBlockSizeException: last block incomplete异常 问题根源
你的代码中TRANSFORMATION使用纯"AES",会默认启用AES/ECB/NoPadding模式,两个核心问题导致了异常:
- ECB模式安全性极低:相同明文块会生成完全相同的密文块,极易被破解;
- NoPadding对块大小要求严格:AES块固定为16字节,NoPadding模式下明文长度必须是16的整数倍。小文件运行正常大概率是刚好满足字节数条件,大文件不满足时,加密后的密文长度不是16的整数倍,解密时就会抛出该异常。
修复方案
推荐使用带填充的CBC模式(兼容性好)或更安全的GCM认证加密模式,以下是具体实现:
方案1:AES/CBC/PKCS5Padding(兼容常见场景)
CBC模式需要随机初始化向量(IV),加密时将IV写入密文头部,解密时先读取IV再解密。
- 修改转换模式常量:
private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding";
- 更新核心
doCrypto方法:
private static void doCrypto(int cipherMode, String key, File inputFile, File outputFile) { try { Key secretKey = generateKey(key); Cipher cipher = Cipher.getInstance(TRANSFORMATION); byte[] iv; FileInputStream inputStream; if (cipherMode == Cipher.ENCRYPT_MODE) { // 生成16字节随机IV iv = new byte[16]; new SecureRandom().nextBytes(iv); cipher.init(cipherMode, secretKey, new IvParameterSpec(iv)); inputStream = new FileInputStream(inputFile); } else { // 解密时先读取文件头部的IV inputStream = new FileInputStream(inputFile); iv = new byte[16]; inputStream.read(iv); cipher.init(cipherMode, secretKey, new IvParameterSpec(iv)); // 跳过已读取的IV,创建新的输入流处理密文内容 inputStream = new FileInputStream(inputFile) { { skip(16); } }; } FileOutputStream outputStream = new FileOutputStream(outputFile); // 加密时先写入IV到文件头部 if (cipherMode == Cipher.ENCRYPT_MODE) { outputStream.write(iv); } // 用8KB缓冲区提升大文件处理效率 byte[] buff = new byte[8192]; int readBytes; while ((readBytes = inputStream.read(buff)) != -1) { outputStream.write(cipher.update(buff, 0, readBytes)); } outputStream.write(cipher.doFinal()); inputStream.close(); outputStream.close(); } catch (Exception ex) { ex.printStackTrace(); } }
方案2:AES/GCM/NoPadding(更安全,支持数据完整性校验)
GCM是AEAD模式,既加密又验证数据完整性,无需填充,适合任意长度数据。需要12字节随机IV,加密时将IV和认证标签写入文件,解密时读取后验证。
- 修改转换模式常量:
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
- 更新核心
doCrypto方法:
private static void doCrypto(int cipherMode, String key, File inputFile, File outputFile) { try { Key secretKey = generateKey(key); Cipher cipher = Cipher.getInstance(TRANSFORMATION); byte[] iv; FileInputStream inputStream; if (cipherMode == Cipher.ENCRYPT_MODE) { // GCM推荐使用12字节IV iv = new byte[12]; new SecureRandom().nextBytes(iv); GCMParameterSpec spec = new GCMParameterSpec(128, iv); cipher.init(cipherMode, secretKey, spec); inputStream = new FileInputStream(inputFile); } else { // 解密时先读取IV和认证标签 inputStream = new FileInputStream(inputFile); iv = new byte[12]; inputStream.read(iv); byte[] tag = new byte[16]; inputStream.read(tag); GCMParameterSpec spec = new GCMParameterSpec(128, iv); cipher.init(cipherMode, secretKey, spec); // 设置需要验证的标签 cipher.updateAAD(tag); // 跳过IV和标签,处理密文内容 inputStream = new FileInputStream(inputFile) { { skip(12 + 16); } }; } FileOutputStream outputStream = new FileOutputStream(outputFile); // 加密时先写入IV if (cipherMode == Cipher.ENCRYPT_MODE) { outputStream.write(iv); } byte[] buff = new byte[8192]; int readBytes; while ((readBytes = inputStream.read(buff)) != -1) { outputStream.write(cipher.update(buff, 0, readBytes)); } byte[] finalBytes = cipher.doFinal(); if (cipherMode == Cipher.ENCRYPT_MODE) { // 写入加密内容和认证标签 outputStream.write(finalBytes); outputStream.write(cipher.getGCMTag()); } else { outputStream.write(finalBytes); } inputStream.close(); outputStream.close(); } catch (Exception ex) { ex.printStackTrace(); } }
额外优化建议
- 禁止使用ECB模式,安全性极低;
- 缓冲区大小建议用8192字节或更大,提升大文件IO效率;
generateKey中的SALT应该用固定的字节数组(如private static final byte[] SALT = "your-fixed-salt-here".getBytes(StandardCharsets.UTF_8);),避免字符编码差异导致密钥不一致;- 不要仅打印异常堆栈,建议根据业务场景抛出自定义异常或进行针对性错误处理。
内容的提问来源于stack exchange,提问作者Михаил Карлов
相关产品推荐
相关产品推荐

