Python AES/CBC加密文件后Android(Java)端解密失败问题求助
问题排查与修复方案
我帮你梳理了Python加密和Android Java解密代码之间的几个关键不匹配点,这些正是导致解密后文件损坏、MD5不一致的核心原因:
1. 填充方式不匹配(最关键问题)
Python加密时手动用空格填充补全块长度,但Java解密时指定了NoPadding,既没有识别这种自定义填充,也没有去除加密时添加的空格:
- Python代码:
chunk += b' ' * (self.BLOCK_SIZE - (len(chunk) % self.BLOCK_SIZE)) - Java代码:
private static final String CIPHER_TRANSFORMATION = "AES/CBC/NoPadding";
这会导致解密后的文件末尾多出不必要的空格,甚至如果原文件最后一块刚好对齐,也会被错误添加空格,直接破坏文件结构。
修复方案:两边统一使用标准的PKCS#7填充(AES默认的标准填充方式):
- Python端:移除手动空格填充,改用PyCryptodome的
Padding模块处理填充 - Java端:把变换模式改为
"AES/CBC/PKCS5Padding"(PKCS5Padding和PKCS7Padding在128位块大小下等价)
2. Java循环中错误调用doFinal()
在解密循环里,每次读取数据都调用cipher.doFinal()是错误的——doFinal()会完成解密并重置Cipher实例,后续的解密操作会直接失败,导致数据损坏。正确的做法是用cipher.update()处理中间块,最后再用doFinal()处理最后一块(包括填充)。
3. 文件大小处理的隐患
- Python中用
zfill(16)生成16位的文件大小字符串,当文件大小超过Integer.MAX_VALUE(约2GB)时,Java端用Integer.parseInt()会抛出数值溢出异常,应该改用Long.parseLong()并以二进制字节形式存储文件大小。 - Java读取
fileSizeBytes时,直接用inputStream.read(fileSizeBytes)不能保证一次性读取16字节,需要循环读取直到填满数组,避免因流读取不完整导致文件大小解析错误。
4. Python中多余的segment_size参数
PyCryptodome的AES.new()中,segment_size参数仅适用于CFB模式,CBC模式下不需要设置,这个参数会被忽略,但最好移除避免混淆。
修改后的代码示例
Python加密代码(修正版)
from Crypto import Random from Crypto.Cipher import AES from Crypto.Util.Padding import pad # 导入标准填充模块 import hashlib import os class AESCipher: MODE = AES.MODE_CBC KEY_BYTES = 32 BLOCK_SIZE = AES.block_size def __init__(self, key: str): self.key = hashlib.sha256(key.encode()).digest() def encrypt_file(self, file_path, output_path): ''' Encrypt file ''' chunk_size = 64 * 1024 file_size = os.path.getsize(file_path) IV = Random.new().read(self.BLOCK_SIZE) encryptor = AES.new(self.key, AES.MODE_CBC, IV) # 移除多余的segment_size参数 with open(file_path, 'rb') as infile: with open(output_path, 'wb') as outfile: # 用8字节大端序存储文件大小,支持超大文件 outfile.write(file_size.to_bytes(8, byteorder='big')) outfile.write(IV) while True: chunk = infile.read(chunk_size) if len(chunk) == 0: break # 中间块直接加密,最后一块用标准填充处理 if len(chunk) == chunk_size: outfile.write(encryptor.encrypt(chunk)) else: outfile.write(encryptor.encrypt(pad(chunk, self.BLOCK_SIZE)))
Android Java解密代码(修正版)
import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.io.UnsupportedEncodingException; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import android.content.Context; import android.util.Log; import java.io.*; public class AESCipher { private static final String AES_ENCRYPTION_ALGORITHM = "AES"; // 改用标准PKCS5填充(与Python的PKCS7兼容) private static final String CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding"; private static final String MESSAGE_DIGEST_ALGORITHM = "SHA-256"; private static final String ENCODING = "UTF-8"; private static final String KEY_STR = "password"; private static final int IV_SIZE = 16; private static final int FILE_SIZE_BYTES = 8; // 对应Python的8字节long类型 private static byte[] KEY; private static AESCipher instance = null; private static final String TAG = "AESCipher"; AESCipher() { try { MessageDigest messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM); messageDigest.update(KEY_STR.getBytes(ENCODING)); KEY = messageDigest.digest(); } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { Log.e(TAG, "初始化密钥失败", e); } } public static AESCipher getInstance() { if (instance == null) { instance = new AESCipher(); } return instance; } public void DecryptFileFromAsset(Context context, String fileName, String outputName) { InputStream inputStream = null; FileOutputStream fileOutputStream = null; try { inputStream = context.getAssets().open(fileName); // 读取8字节的文件大小(大端序) byte[] fileSizeBytes = new byte[FILE_SIZE_BYTES]; readFully(inputStream, fileSizeBytes); long fileSize = bytesToLong(fileSizeBytes); // 读取IV byte[] ivBytes = new byte[IV_SIZE]; readFully(inputStream, ivBytes); // 初始化AES cipher IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBytes); SecretKeySpec keySpec = new SecretKeySpec(KEY, AES_ENCRYPTION_ALGORITHM); Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivParameterSpec); File file = new File(context.getFilesDir() + File.separator + outputName); if (!file.exists()) { file.createNewFile(); } fileOutputStream = new FileOutputStream(file); byte[] encryptedBytes = new byte[64 * 1024]; int readLen; // 用update处理中间块 while ((readLen = inputStream.read(encryptedBytes)) != -1) { byte[] decryptedChunk = cipher.update(encryptedBytes, 0, readLen); if (decryptedChunk != null) { fileOutputStream.write(decryptedChunk); } } // 处理最后一块(自动去除填充) byte[] finalChunk = cipher.doFinal(); if (finalChunk != null) { fileOutputStream.write(finalChunk); } // 截断文件到原大小,避免极端情况的填充残留 fileOutputStream.getChannel().truncate(fileSize); } catch (Exception e) { Log.e(TAG, "解密文件失败", e); } finally { try { if (inputStream != null) inputStream.close(); if (fileOutputStream != null) fileOutputStream.close(); } catch (IOException e) { Log.e(TAG, "关闭流失败", e); } } } // 辅助方法:确保读取指定长度的字节,避免流读取不完整 private void readFully(InputStream in, byte[] buffer) throws IOException { int totalRead = 0; int remaining = buffer.length; while (remaining > 0) { int read = in.read(buffer, totalRead, remaining); if (read == -1) { throw new IOException("流提前结束,无法读取足够字节"); } totalRead += read; remaining -= read; } } // 辅助方法:将8字节大端数组转为long private long bytesToLong(byte[] bytes) { long result = 0; for (byte b : bytes) { result = (result << 8) | (b & 0xFF); } return result; } }
额外说明
- 改用8字节存储文件大小,支持最大9EB的文件,彻底避免了Integer溢出问题。
- 新增
readFully方法确保读取完整的文件大小和IV,解决流读取不完整导致的解析错误。 - Java端使用
update+doFinal的正确解密流程,保证数据完整性。 - 统一使用标准填充方式,避免手动填充带来的各种边缘错误。
内容的提问来源于stack exchange,提问作者Linmeimei
相关产品推荐
相关产品推荐

