Python流式AES加密解密实战:aes-pipe库详解与应用案例

Python流式AES加密解密实战:aes-pipe库详解与应用案例

1. 项目概述

在数据处理和系统开发的日常工作中,我们经常遇到一个看似简单却至关重要的需求:如何安全、高效地处理流式数据?比如,你正在开发一个日志分析工具,需要实时加密从服务器管道传输过来的日志流;或者你有一个大文件,不想全部加载到内存,而是希望边读取边加密并写入另一个文件。这时候,如果直接使用像pycryptodome这样的标准库,你需要手动处理分块、填充和模式,代码会变得冗长且容易出错。而aes-pipe这个 Python 包,就是为了解决这类“流式 AES 加密/解密”痛点而生的利器。

简单来说,aes-pipe不是一个全新的加密算法实现,而是一个精巧的“管道”封装。它将强大的 AES 对称加密算法与 Python 内置的io模块和类文件对象接口无缝结合,让你能够像操作普通文件一样,对数据进行透明的加密和解密。你不再需要关心每次读取多少字节、PKCS7填充如何手动添加或移除、CBC模式的初始化向量如何传递——这些底层细节都被aes-pipe优雅地隐藏了起来。对于需要处理网络流、大文件或任何类文件对象加密场景的开发者而言,它极大地简化了代码复杂度,提升了开发效率和代码的可维护性。接下来,我将带你深入这个包的语法、核心参数,并通过几个接地气的实际案例,展示如何将它应用到你的项目中。

2. aes-pipe 核心设计与思路拆解

2.1 为什么需要 aes-pipe?标准库的痛点

在深入aes-pipe之前,我们先看看使用标准加密库(如cryptographypycryptodome)处理流式加密的典型流程:

  1. 分块处理:AES 是块加密算法,一次只能处理固定长度(如 16 字节)的数据。对于任意长度的数据,你必须手动将其分割成块。
  2. 填充(Padding):如果最后一块数据不足一个块长度,你需要进行填充(常用 PKCS7)。加密时需要添加,解密后需要移除。
  3. 模式与IV:如果使用 CBC 等模式,你需要生成并管理初始化向量,并且在解密时正确传递它。
  4. 流式整合:你需要在一个循环中,不断从源(如文件对象、网络套接字)读取数据块,加密后写入目标,同时处理好最后一块的填充。

这个过程虽然可控,但代码模板化严重,且容易在边界条件(如文件末尾、空流)上出错。aes-pipe的设计思路就是将这些繁琐的步骤封装起来,提供一个实现了 Python 文件对象协议(即具有read,write,seekable等方法)的包装器。你只需要用密钥和参数初始化这个包装器,然后就可以像读写普通文件一样操作它,加密和解密在后台自动完成。

2.2 aes-pipe 的架构与核心思想

aes-pipe的核心是AESStream类(或其变体,如AESReader,AESWriter)。它的设计遵循了“装饰器”或“包装器”模式:

  • 输入/输出抽象:它将一个底层的“类文件对象”(如open()返回的文件对象、BytesIO内存对象、甚至是网络响应流)作为输入。
  • 透明转换:在你通过read()方法读取数据时,它从底层对象读取密文,实时解密后返回明文给你。在你通过write()方法写入数据时,它将你提供的明文实时加密后写入底层对象。
  • 状态管理:它内部维护了加密/解密所需的上下文,包括块缓冲区、填充状态、IV(对于需要IV的模式)等,对外完全隐藏了这些细节。

这种设计带来了几个显著优势:

  • 接口统一:学习成本极低,任何会使用 Python 文件操作的人都能立即上手。
  • 内存友好:数据是流式处理的,无需将整个文件内容加载到内存,非常适合处理大文件。
  • 易于集成:可以轻松插入到任何使用文件对象协议的现有代码或框架中,比如与shutil.copyfileobj配合进行文件加密拷贝,或者作为requests响应流的内容过滤器。

3. 安装、基础语法与核心参数详解

3.1 安装与环境准备

aes-pipe的安装非常简单,通过 pip 即可完成。它通常依赖于pycryptodome这个强大的密码学库作为后端。

pip install aes-pipe

安装完成后,你可以在代码中直接导入:

from aes_pipe import AESStream # 或者根据版本不同,也可能是 from aes_pipe import AesPipe

注意:由于aes-pipe本身可能更新,且其底层依赖pycryptodome,建议在正式项目中使用时,在requirements.txt中固定版本号,以避免因版本升级导致的潜在兼容性问题。

3.2 核心类:AESStream 初始化参数详解

AESStream是主要的入口类。其初始化函数__init__的参数决定了加密/解密的行为。理解这些参数是正确使用的关键。

# 典型的初始化语句 cipher_stream = AESStream( key, mode='CBC', iv=None, segment_size=16, padding='pkcs7', input_stream=None, output_stream=None, for_reading=True )

下面我们逐一拆解每个参数:

  1. key(必需)

    • 作用:加密和解密使用的密钥。长度决定了 AES 的强度:16字节(128位)、24字节(192位)或32字节(256位)。
    • 注意事项:密钥必须妥善保存。在实际应用中,密钥不应硬编码在代码里,而应从环境变量、密钥管理服务或经过安全加密的配置文件中读取。生成密钥可以使用os.urandom(16)生成随机字节。
  2. mode(可选,默认:'CBC')

    • 作用:指定 AES 的工作模式。aes-pipe支持常见的模式,如'CBC','CFB','OFB','CTR','ECB'
    • 模式选择建议
      • CBC:最常用的模式之一,需要初始化向量,安全性好,但无法并行加密。适合文件加密。
      • CTR:计数器模式,将块密码转换为流密码。不需要填充,可以并行加密/解密,非常适合流式数据和高性能场景。这是处理流数据的推荐模式之一
      • ECB:电子密码本模式,相同的明文块会产生相同的密文块,不推荐用于加密有意义的数据,因为它不能很好地隐藏数据模式。除非有特殊兼容性要求,否则应避免使用。
    • 为什么重要:模式直接影响加密的安全性、性能和是否需要对数据进行填充。
  3. iv(初始化向量,对于 CBC, CFB, OFB 模式必需)

    • 作用:一个随机或伪随机的值,用于确保即使相同的明文用相同的密钥加密,也会产生不同的密文。对于 CBC 模式,其长度必须为 16 字节。
    • 生成与存储:IV 不需要保密,但必须唯一(对于同一个密钥)。通常与密文一起存储或传输。你可以用os.urandom(16)生成。在解密时,必须使用加密时相同的 IV。
    • CTR模式的iv:在 CTR 模式中,这个参数通常被称为nonce(随机数),它与计数器一起构成初始计数器块。其长度可以小于 16 字节,剩余部分用于计数器。
  4. segment_size(可选,默认:16)

    • 作用:对于 CFB 和 OFB 模式,定义一次处理的位数(单位是位,不是字节)。例如,segment_size=8表示每次处理 1 字节。对于 CBC 和 ECB 模式,此参数固定为 128(即 16 字节),设置无效。
    • 实际影响:较小的segment_size(如 8)可以将块密码转换为更接近“流密码”的行为,但可能会略微降低性能。除非有特定协议要求,通常使用默认值即可。
  5. padding(可选,默认:'pkcs7')

    • 作用:指定当数据长度不是块长度的整数倍时,使用的填充方案。
    • 可选值:常见的有'pkcs7'(默认且最常用)、'iso7816''x923''zero'等,也支持None(无填充)。
    • 重要规则如果模式是CTR,CFB,OFB等流密码模式,或者segment_size不是 128,则必须设置padding=None,因为这些模式本身不需要填充。如果设置了填充,aes-pipe会尝试移除填充,可能导致解密失败。
    • pkcs7原理:假设块长度为 16 字节,如果最后一块差 N 个字节,则用数值 N 填充 N 次。例如,差3字节,则填充\x03\x03\x03
  6. input_stream/output_stream(核心参数)

    • 作用:这是aes-pipe“管道”概念的体现。你需要将一个已打开的、可读或可写的类文件对象传递给它。
      • input_stream:当for_reading=True时,AESStream会从这个流中读取密文,解密后通过read()方法返回明文给你。
      • output_stream:当for_reading=False时,你通过write()方法写入的明文,会被加密后写入这个流,成为密文
    • 类型:任何具有read(size)write(data)方法的对象都可以,如:open()返回的文件对象、io.BytesIOio.StringIO(注意编码)、socket.makefile()创建的套接字文件对象等。
  7. for_reading(可选,默认:True)

    • 作用:明确指定这个AESStream对象是用于读取(解密)还是写入(加密)。
    • for_reading=True:对象处于“读取模式”。你从它read()得到的是解密后的明文。你必须提供input_stream
    • for_reading=False:对象处于“写入模式”。你向它write()明文,它负责加密并写入output_stream。你必须提供output_stream

3.3 基础操作语法

创建好AESStream对象后,你就可以像使用普通文件对象一样操作它。

读取(解密)模式:

# 假设 ciphertext.bin 是一个已加密的文件 key = b'my-16byte-key....' # 32字节 for AES-256 iv = b'initial-vector...' # 16字节 for CBC with open('ciphertext.bin', 'rb') as f_in: # 创建解密流 with AESStream(key, mode='CBC', iv=iv, input_stream=f_in, for_reading=True) as decryptor: plain_data = decryptor.read() # 读取全部并解密 # 或者循环读取 # while chunk := decryptor.read(1024): # process(chunk)

写入(加密)模式:

key = b'my-16byte-key....' iv = b'initial-vector...' with open('encrypted.bin', 'wb') as f_out: # 创建加密流 with AESStream(key, mode='CBC', iv=iv, output_stream=f_out, for_reading=False) as encryptor: encryptor.write(b'This is my secret plain text.') # 可以多次 write encryptor.write(b'More plain data...') # 退出 with 块时,加密流会自动关闭并处理最后的填充(如果需要)

实操心得:强烈建议使用with语句来管理AESStream对象及其底层流的生命周期。这能确保在发生异常或操作完成后,文件描述符被正确关闭,缓冲区被正确刷新(对于写入模式,这尤其重要,否则可能丢失最后的加密块)。

4. 实际应用案例深度解析

理解了核心参数后,我们通过几个实际场景来巩固用法。这些案例覆盖了文件操作、内存流和网络流,都是开发中常见的情形。

4.1 案例一:大文件加密与解密(本地文件)

这是最直接的应用。假设你有一个几百兆的日志文件app.log,需要加密后存档为app.log.enc,并在必要时解密。

步骤1:加密文件

import os from aes_pipe import AESStream def encrypt_file(input_file_path, output_file_path, key, iv): """ 加密文件 """ # 生成随机的密钥和IV(这里示例,实际应从安全的地方获取) # key = os.urandom(32) # AES-256 # iv = os.urandom(16) # for CBC mode with open(input_file_path, 'rb') as f_in, open(output_file_path, 'wb') as f_out: # 创建加密流。注意 for_reading=False with AESStream(key, mode='CBC', iv=iv, output_stream=f_out, for_reading=False) as encryptor: # 使用 shutil 高效拷贝,或者手动循环 chunk_size = 1024 * 64 # 64KB 块 while True: chunk = f_in.read(chunk_size) if not chunk: break encryptor.write(chunk) print(f"文件加密完成: {output_file_path}") # 注意:IV 需要和密文一起保存,否则无法解密! # 通常将 IV 写入密文文件的开头。 with open(output_file_path + '.iv', 'wb') as f_iv: f_iv.write(iv) # 使用示例 key = os.urandom(32) iv = os.urandom(16) encrypt_file('app.log', 'app.log.enc', key, iv)

关键点

  1. 我们以二进制模式('rb','wb')打开文件。
  2. 将输出文件对象f_out传给AESStream作为output_stream
  3. 使用循环分块读取源文件并写入加密流,避免内存耗尽。
  4. IV 必须保存!这里我们将其单独存为一个.iv文件。另一种常见做法是将 IV 作为密文文件的前 16 个字节写入。

步骤2:解密文件

def decrypt_file(input_file_path, output_file_path, key, iv): """ 解密文件 """ with open(input_file_path, 'rb') as f_in, open(output_file_path, 'wb') as f_out: # 创建解密流。注意 for_reading=True with AESStream(key, mode='CBC', iv=iv, input_stream=f_in, for_reading=True) as decryptor: chunk_size = 1024 * 64 while True: chunk = decryptor.read(chunk_size) if not chunk: break f_out.write(chunk) print(f"文件解密完成: {output_file_path}") # 使用示例:先从 .iv 文件读取 IV with open('app.log.enc.iv', 'rb') as f: saved_iv = f.read() decrypt_file('app.log.enc', 'app.log.decrypted', key, saved_iv)

注意事项:加解密使用的key,mode,iv必须完全一致。CBC模式需要填充,aes-pipe默认使用pkcs7,在解密时会自动移除填充,所以我们的解密循环可以一直读到空块为止。

4.2 案例二:内存中数据的加密与解密(使用 BytesIO)

有时数据并不在文件中,而是在内存变量或网络请求的响应里。io.BytesIO是一个在内存中模拟文件行为的类,与aes-pipe是绝配。

场景:你的 Web 应用接收到一段 JSON 敏感数据,需要先加密后再存入数据库的 BLOB 字段。从数据库读取后,再解密使用。

import io import json from aes_pipe import AESStream import base64 # 为了方便存储,可能将二进制密文转为Base64 def encrypt_in_memory(plain_data: bytes, key: bytes, iv: bytes) -> bytes: """在内存中加密数据,返回密文字节串""" # 创建一个 BytesIO 对象作为输出“文件” cipher_output = io.BytesIO() with AESStream(key, mode='CBC', iv=iv, output_stream=cipher_output, for_reading=False) as encryptor: encryptor.write(plain_data) # 获取加密后的全部数据 # 注意:加密流关闭后,缓冲区数据才会完全写入底层流。 cipher_data = cipher_output.getvalue() cipher_output.close() return cipher_data def decrypt_in_memory(cipher_data: bytes, key: bytes, iv: bytes) -> bytes: """在内存中解密数据,返回明文字节串""" # 创建一个包含密文的 BytesIO 对象作为输入“文件” cipher_input = io.BytesIO(cipher_data) with AESStream(key, mode='CBC', iv=iv, input_stream=cipher_input, for_reading=True) as decryptor: plain_data = decryptor.read() # 一次性读取所有解密数据 cipher_input.close() return plain_data # 模拟使用 key = os.urandom(32) iv = os.urandom(16) # 原始敏感数据(比如一个配置字典) sensitive_config = {'api_key': 'supersecret123', 'user_id': 1001} plain_json = json.dumps(sensitive_config).encode('utf-8') print(f"原始明文: {plain_json}") # 加密 cipher_bytes = encrypt_in_memory(plain_json, key, iv) print(f"加密后 (hex): {cipher_bytes.hex()[:50]}...") # 可以转成Base64存入数据库 cipher_b64 = base64.b64encode(cipher_bytes).decode('ascii') print(f"加密后 (Base64): {cipher_b64[:50]}...") # 解密(模拟从数据库读取) # 假设我们从数据库拿到的是Base64字符串和IV cipher_bytes_from_db = base64.b64decode(cipher_b64) decrypted_bytes = decrypt_in_memory(cipher_bytes_from_db, key, iv) decrypted_json = json.loads(decrypted_bytes.decode('utf-8')) print(f"解密后数据: {decrypted_json}") assert decrypted_json == sensitive_config, "加解密数据不一致!"

优势:整个过程数据完全在内存中流转,没有磁盘 I/O,速度极快。BytesIO提供了文件接口,使得aes-pipe可以无缝工作。

4.3 案例三:网络流加密(结合 requests 库)

考虑一个场景:你需要从一个安全的 API 下载一个加密的大文件,并边下载边解密保存到本地,而不是等整个文件下载完再解密。这可以节省内存和临时磁盘空间。

我们将结合requests库的流式下载功能。

import requests from aes_pipe import AESStream def download_and_decrypt_stream(url, output_path, key, iv): """ 流式下载加密文件并实时解密保存 """ # 发起一个流式请求 response = requests.get(url, stream=True) response.raise_for_status() # 确保请求成功 # 以二进制写入模式打开本地文件 with open(output_path, 'wb') as f_out: # 关键:将 HTTP 响应体(一个流)作为 input_stream # 注意:response.raw 或 response.iter_content() 可以作为类文件对象。 # 但 response.raw 可能默认不解码,更接近原始socket流。 # 我们使用 response.iter_content(chunk_size=None) 来获取一个生成器,但AESStream需要类文件对象。 # 更直接的方法:使用 response.raw 并设置 stream=True # 但需要注意,response.raw 可能有额外的包装。一个更稳妥的方法是手动迭代。 # 方法A(推荐):创建一个自定义的类文件对象包装响应流(简化示例) class StreamingBodyWrapper: def __init__(self, response_iter): self.iter = response_iter self.buffer = b'' def read(self, size=-1): if size == -1: # 读取全部,将迭代器内容合并 return b''.join(self.iter) # 否则,从缓冲区或迭代器中读取指定大小 while len(self.buffer) < size: try: chunk = next(self.iter) self.buffer += chunk except StopIteration: break data, self.buffer = self.buffer[:size], self.buffer[size:] return data # 创建响应内容的迭代器,chunk_size=None 表示使用库认为合适的块大小 content_iter = response.iter_content(chunk_size=None) stream_wrapper = StreamingBodyWrapper(content_iter) # 创建解密流,输入是我们的包装器 with AESStream(key, mode='CBC', iv=iv, input_stream=stream_wrapper, for_reading=True) as decryptor: # 从解密流读取并写入文件 chunk_size = 1024 * 32 while True: chunk = decryptor.read(chunk_size) if not chunk: break f_out.write(chunk) print(f"文件已下载并解密至: {output_path}") # 使用示例(假设有一个返回加密文件流的API端点) # key 和 iv 需要从安全渠道获取,可能与URL或认证信息一起下发。 # iv 可能包含在HTTP响应的头部,例如 `X-IV: <base64_iv>` api_url = "https://api.example.com/secure-download/encrypted-file.bin" key = b'...' # 从配置或密钥服务获取 iv = b'...' # 可能从响应头 `X-IV` 中获取并解码 download_and_decrypt_stream(api_url, 'decrypted_local_file.bin', key, iv)

重要提示:这个例子中的StreamingBodyWrapper是一个简化版,用于说明原理。在实际生产中,你需要更健壮的实现来处理边界条件、超时和错误。requestsresponse.raw在某些条件下(stream=Truedecode_content=False)可以作为一个类文件对象直接使用,但要注意其内部缓冲和编码处理。这个案例展示了aes-pipe如何与任何符合“类文件对象”协议的流集成,思路是通用的。

4.4 案例四:使用 CTR 模式处理流媒体数据

对于像音频、视频片段这样的流媒体数据,或者需要随机访问部分加密数据的场景,CTR模式是更好的选择,因为它不需要填充,并且可以将加密转换为流密码操作。

import os from aes_pipe import AESStream def encrypt_with_ctr(input_stream, output_stream, key, nonce): """ 使用 CTR 模式加密流。nonce 相当于其他模式中的 iv,但用法不同。 """ # CTR 模式不需要填充 # 在 pycryptodome 底层,CTR 模式需要一个完整的 16 字节计数器块。 # aes-pipe 的 `iv` 参数在这里作为 nonce 使用。 # 通常,nonce 长度 + 计数器长度 = 16 字节。 # 例如,8字节nonce + 8字节计数器。aes-pipe/pycryptodome 会帮你处理。 # 我们这里假设 nonce 是 8 字节。 with AESStream(key, mode='CTR', iv=nonce, padding=None, output_stream=output_stream, for_reading=False) as encryptor: chunk_size = 8192 while True: chunk = input_stream.read(chunk_size) if not chunk: break encryptor.write(chunk) def decrypt_with_ctr(input_stream, output_stream, key, nonce): """使用 CTR 模式解密流。解密过程与加密完全相同。""" with AESStream(key, mode='CTR', iv=nonce, padding=None, input_stream=input_stream, for_reading=True) as decryptor: chunk_size = 8192 while True: chunk = decryptor.read(chunk_size) if not chunk: break output_stream.write(chunk) # 示例:加密一个MP3文件的前1MB(模拟处理媒体流) key = os.urandom(32) # AES-256 nonce = os.urandom(8) # 8字节 nonce for CTR with open('sample.mp3', 'rb') as f_in, open('sample_encrypted.ctr', 'wb') as f_out: # 只处理前 1MB 作为演示 limited_stream = io.BytesIO(f_in.read(1024 * 1024)) encrypt_with_ctr(limited_stream, f_out, key, nonce) print("CTR模式加密完成。") print("注意:nonce 必须安全保存,用于解密。")

CTR 模式的优势

  • 无填充:数据长度可以任意,加密后密文长度与明文完全相同。
  • 可并行:计数器可以预测,允许并行加密/解密多个块。
  • 随机访问:由于每个块的加密只依赖于其在流中的位置(计数器),你可以解密文件的中间某一段,而无需从头开始。这对于媒体文件非常有用。

5. 常见问题、排查技巧与性能优化

在实际使用aes-pipe时,你可能会遇到一些典型问题。下面是我踩过的一些坑和总结的排查思路。

5.1 常见错误与解决方案

问题现象可能原因解决方案与排查步骤
ValueError: Data must be padded to ...或解密后末尾出现乱码1. 加解密使用的模式/填充不一致。
2. 使用了需要填充的模式(如CBC),但解密时数据长度不是块大小的整数倍(密文可能被截断或损坏)。
3.CTR/CFB/OFB等流模式错误地设置了填充。
1.核对参数:确保key,mode,iv,padding在加密和解密时完全一致。
2.检查数据完整性:确保密文在传输或存储过程中没有丢失字节。可以对比MD5/SHA256哈希值。
3.流模式禁用填充:对于CTR,CFB,OFB模式,务必设置padding=None
TypeError: argument should be bytes-like object...write()方法传递了字符串(str)而非字节(bytes)。在写入前,使用.encode('utf-8')将字符串转换为字节。确保所有与AESStream交互的数据都是bytes类型。
解密出来的数据开头多出一些字符或不对IV 不匹配。这是 CBC 模式最常见的问题。加密时生成的 IV 和解密时使用的 IV 不同。1.确认 IV 存储与读取:如果你将 IV 存储在文件开头,确保读取的偏移量正确。例如,前16字节是IV,后面才是密文。
2.代码检查:仔细检查加密和解密两处的iv变量值是否来自同一个源。
AESStreamwith块结束后,输出文件为空或不全写入模式下,数据可能还在内部缓冲区,没有完全刷新到底层流。虽然with语句会调用close(),但最好显式处理。1.确保使用with语句
2. 在write()循环结束后,可以尝试调用一次encryptor.flush()(如果对象支持)。
3. 对于自定义的类文件对象,确保其write方法正确实现。
性能不佳,处理大文件非常慢使用的块大小(chunk_size)不合适。太小会导致频繁的IO和加密调用;太大可能占用过多内存且收益递减。调整chunk_size:通过实验找到一个平衡点。对于本地文件,64KB 到 1MB 通常是较好的范围。可以使用timeit模块测试不同块大小的耗时。
内存使用过高(当使用BytesIO时)使用decryptor.read()不加参数,会一次性将解密后的所有数据读入内存。如果原始密文很大,内存就会爆掉。流式处理:即使源是BytesIO,也采用循环分块读取的方式:while chunk := decryptor.read(1024*64): ...

5.2 性能优化与最佳实践

  1. 选择合适的块大小:在读写循环中(f_in.read(chunk_size)),chunk_size直接影响性能。建议设置为 4096 的倍数(如 65536),这通常与文件系统块大小和加密块大小对齐。可以通过小规模测试来确定最优值。

  2. 使用CTR模式处理流:如果你的数据是真正的“流”(无明确边界,长度未知,或需要随机访问),CTR模式是首选。它无需填充,且加密解密为相同的操作,逻辑更简单。

  3. 密钥与 IV 管理

    • 密钥:绝对不要硬编码。使用环境变量、密钥管理服务或硬件安全模块。
    • IV:必须唯一且不可预测。对于 CBC 模式,每次加密都应使用新的随机 IV。IV 可以公开,通常与密文一起存储(如放在文件开头、数据库字段或HTTP头中)。
  4. 错误处理:加密解密操作可能因数据损坏、密钥错误而失败。务必使用try...except块包裹核心逻辑,并记录或处理可能的异常(如ValueError,TypeError,EOFError等)。

  5. 测试与验证:编写单元测试,用已知的明文、密钥和 IV 进行加密,然后解密,验证是否能还原。这能确保你的整个流程和参数设置是正确的。

5.3 一个综合的、健壮的加密工具函数示例

结合以上所有经验,这里提供一个更健壮的文件加密函数,它包含了错误处理、IV 存储和进度提示。

import os import hashlib from aes_pipe import AESStream def robust_file_encrypt(source_path, dest_path, key): """ 健壮的文件加密函数。 将IV存储在目标文件的前16字节。 返回用于解密的IV。 """ if not os.path.exists(source_path): raise FileNotFoundError(f"源文件不存在: {source_path}") iv = os.urandom(16) # 为每次加密生成新的IV source_file_size = os.path.getsize(source_path) try: with open(source_path, 'rb') as f_in, open(dest_path, 'wb') as f_out: # 1. 先将IV写入目标文件开头 f_out.write(iv) # 2. 创建加密流,指向目标文件(从当前位置开始写) with AESStream(key, mode='CBC', iv=iv, output_stream=f_out, for_reading=False) as encryptor: processed = 0 chunk_size = 1024 * 128 # 128KB while True: chunk = f_in.read(chunk_size) if not chunk: break encryptor.write(chunk) processed += len(chunk) # 可以在这里添加进度回调 # if callback: # callback(processed, source_file_size) print(f"加密成功: {source_path} -> {dest_path}") # 计算源文件哈希,用于后续验证(可选) # with open(source_path, 'rb') as f: # source_hash = hashlib.sha256(f.read()).hexdigest() # print(f"源文件SHA256: {source_hash}") return iv except Exception as e: # 如果出错,尝试清理可能已部分写入的目标文件 if os.path.exists(dest_path): os.remove(dest_path) raise RuntimeError(f"文件加密过程中出错: {e}") from e def robust_file_decrypt(source_path, dest_path, key): """ 健壮的文件解密函数。 从源文件的前16字节读取IV。 """ if not os.path.exists(source_path): raise FileNotFoundError(f"加密文件不存在: {source_path}") if os.path.getsize(source_path) <= 16: raise ValueError("加密文件过小或已损坏") try: with open(source_path, 'rb') as f_in, open(dest_path, 'wb') as f_out: # 1. 从文件开头读取IV iv = f_in.read(16) # 2. 创建解密流,源文件从第17字节开始是密文 with AESStream(key, mode='CBC', iv=iv, input_stream=f_in, for_reading=True) as decryptor: processed = 0 chunk_size = 1024 * 128 while True: chunk = decryptor.read(chunk_size) if not chunk: break f_out.write(chunk) processed += len(chunk) print(f"解密成功: {source_path} -> {dest_path}") except ValueError as e: # 常见的解密错误:密钥错误、IV错误、数据损坏、填充错误 if os.path.exists(dest_path): os.remove(dest_path) raise RuntimeError(f"解密失败,请检查密钥和文件完整性。错误详情: {e}") from e except Exception as e: if os.path.exists(dest_path): os.remove(dest_path) raise RuntimeError(f"文件解密过程中出错: {e}") from e # 使用示例 key = os.urandom(32) # 请安全保管此密钥 iv = robust_file_encrypt('report.pdf', 'report.pdf.enc', key) print(f"本次加密使用的IV (hex): {iv.hex()}") # 稍后解密 robust_file_decrypt('report.pdf.enc', 'report_decrypted.pdf', key)

这个函数将 IV 作为密文的一部分管理,避免了单独存储 IV 文件的麻烦,并且加入了基本的错误处理和资源清理,更适合在生产环境中使用。