基于AES-GCM的文件分块加密实现正确性及MAC计算问询
AES-GCM分块加密文件的正确性问题及解决方案
问题描述
我希望编写一个生成器,将文件按指定大小分块加密并逐个返回加密块;由于需要对载荷进行认证,因此选择了AES-GCM算法。
选择分块加密而非一次性加密整个文件的原因是:我需要将这些块通过网络传输,若先加密整个(可能很大的)文件并存储,传输时再分块会冗余,因此直接加密待传输的单个块并立即发送。
但我不确定当前实现是否正确,且未找到对应场景的实用文档,以下是我的代码:
from Crypto.Cipher import AES from Crypto import Random import struct CHUNK_SIZE = 32 AES_BLOCK_SIZE = 16 def encryptFileInChunks(key: bytes, infile: str, file_size: int): iv = Random.new.read(16) encryptor = AES.new(key, AES.MODE_GCM, iv, mac_len=16) with open(infile, 'r') as infile: # write the original file size as little endian 8 byte int # so that we can remove the padding after decryption by # truncating to this original length yield struct.pack("<Q", file_size) # iv for decryption yield iv while True: chunk = infile.read(CHUNK_SIZE) if len(chunk) == 0: break if len(chunk) % AES_BLOCK_SIZE != 0: # add padding to make a valid block chunk += b" " * (AES_BLOCK_SIZE - len(chunk) % AES_BLOCK_SIZE) c, d = encryptor.encrypt_and_digest(chunk) # only return ciphertext yield c, None # first tuple value being None indicates end of ciphertext, # return the mac yield None, d
请问该实现是否能按照GCM模式的要求正确计算整个载荷的MAC,还是仅返回最后一个块的MAC?如何以类似方式正常计算整个密文的MAC,而无需为每个块单独发送(密文, MAC)对?
我考虑过使用AES-CBC搭配HMAC单独认证,但这会增加少许开销,而AES-GCM的速度更快。
问题分析与解决方案
当前实现的核心错误
- 文件操作缩进错误:
while循环位于with open块外部,此时文件已经被关闭,无法读取任何内容,直接导致加密逻辑无法执行。 - GCM模式使用错误:
- GCM是流加密模式,不需要对明文进行填充,它原生支持任意长度的输入。你添加的填充不仅多余,还会导致解密后需要额外处理去掉填充,增加复杂度。
encrypt_and_digest()方法会一次性处理输入并生成对应的MAC,且每次调用都会终止当前加密上下文。你对每个块调用该方法,得到的是单个块的MAC,而非整个文件的全局MAC,完全不符合GCM的认证要求。
正确实现
以下是修正后的代码,它会分块加密文件,最终生成整个文件的全局MAC,同时保持逐块输出密文的特性:
from Crypto.Cipher import AES from Crypto import Random import struct CHUNK_SIZE = 32 def encryptFileInChunks(key: bytes, infile_path: str, file_size: int): # GCM推荐使用12字节IV(96位),兼顾性能与安全性,16字节IV也兼容 iv = Random.new().read(12) encryptor = AES.new(key, AES.MODE_GCM, iv, mac_len=16) with open(infile_path, 'rb') as infile: # 先发送原始文件大小(用于解密后截断)和IV yield struct.pack("<Q", file_size) yield iv # 逐块读取并加密 while True: chunk = infile.read(CHUNK_SIZE) if not chunk: break # 分块加密,累积MAC计算上下文但不生成最终MAC ciphertext_chunk = encryptor.encrypt(chunk) yield ciphertext_chunk, None # 所有块处理完成后,生成全局MAC mac = encryptor.digest() yield None, mac
关键说明
- 文件读取:将
while循环放入with块内部,确保文件在读取过程中保持打开状态;同时使用'rb'模式读取文件,避免文本模式的编码问题。 - GCM的正确用法:
- 使用
encrypt()方法分块处理明文,该方法会累积计算MAC所需的上下文数据,但不会生成最终MAC。 - 所有明文块处理完成后,调用
digest()生成整个文件的全局MAC,这个MAC是基于所有明文块、IV和密钥计算得出的,完全符合GCM的认证要求。
- 使用
- IV选择:GCM推荐使用12字节IV,能让实现更高效且避免某些安全隐患,16字节IV也合法但性能略低。
- 无需填充:GCM对任意长度的明文都能处理,不需要额外添加填充,解密时直接根据原始文件大小截断即可。
解密端对应逻辑(补充参考)
解密时需要先接收文件大小和IV,然后逐块接收密文并解密,最后接收MAC并验证:
def decryptFileInChunks(key: bytes, outfile_path: str): # 此处模拟网络接收逻辑,实际场景需替换为真实的数据流读取 def receive_data(): # 示例:从网络或其他来源获取加密后的数据块 pass data_iter = receive_data() # 接收文件大小和IV file_size_bytes = next(data_iter) file_size = struct.unpack("<Q", file_size_bytes)[0] iv = next(data_iter) decryptor = AES.new(key, AES.MODE_GCM, iv, mac_len=16) with open(outfile_path, 'wb') as outfile: while True: chunk_data = next(data_iter) if chunk_data[0] is None: # 接收MAC并验证完整性 mac = chunk_data[1] try: decryptor.verify(mac) except ValueError: raise Exception("MAC验证失败,数据可能被篡改或损坏") break # 解密块并写入文件 plaintext_chunk = decryptor.decrypt(chunk_data[0]) outfile.write(plaintext_chunk) # 截断到原始文件大小,确保与原文件一致 outfile.truncate(file_size)
内容的提问来源于stack exchange,提问作者vibhav950
相关产品推荐
相关产品推荐

