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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.29 20:52:45