Python 实现高效的 SM4 大文件加密解密实战指南20241024
Python 实现高效的 SM4 大文件加密解密实战指南
引言
在数据安全领域,使用对称加密算法如SM4进行数据保护非常常见。特别是当处理大文件时,合理的内存和块大小管理以及加密解密效率变得尤为重要。本文将分享如何使用Python进行大文件的SM4加密解密操作,并对代码进行优化,加入异常处理和内存管理措施,确保程序在处理大文件时的稳定性。
目录
- 背景介绍
- SM4算法概述
- 挑战与解决方案
- 填充处理的重要性
- 大文件的分块处理
- 完整代码实现
- 环境准备
- AlgorithmTool 类的实现
- SM4FileHelper 类的实现(含详细注释)
- 代码解析与说明
- 填充与去填充
- 按块加密解密逻辑
- 处理最后一个数据块
- 最佳实践与注意事项
- 内存优化
- 异常处理
- 安全性考虑
- 总结
背景介绍
SM4 是我国国家密码管理局制定的对称加密算法,被广泛应用于数据保护领域。然而,在对大文件进行加密解密时,如果不合理管理内存和块的大小,程序可能会崩溃或变得非常缓慢。我们将通过优化代码和加入合适的异常处理机制,提升程序的鲁棒性和性能。
SM4 算法概述
SM4 是一种分组对称加密算法,其块大小和密钥长度均为128位(16字节)。它的安全性和高效性使其成为移动通信、电子政务等领域的首选算法。
挑战与解决方案
初始问题
在使用SM4进行大文件加密解密时,您可能遇到以下问题:
- 加密后的文件大小与原始文件大小相同:加密后的文件应该比原文件略大,这是由于填充机制导致的。
- 解密后的文件内容不正确,无法还原到原始文件。
问题分析
通过代码分析,我们发现了以下问题:
- 填充处理不当:加密过程中,最后一个数据块没有正确处理填充,导致加密数据的长度与原始数据不匹配。
- 数据块未对齐:SM4 的块大小是16字节,但在加密和解密时使用了不同的块大小,导致数据块未正确对齐。
解决方案
通过以下改进,我们修复了这些问题:
- 调整数据块的大小:使用16的倍数作为块大小(例如16 * 1024字节),确保数据块与SM4的块大小对齐。
- 正确处理填充:仅对最后一个数据块进行填充,解密时正确去除填充。
- 添加异常处理:引入合理的异常处理机制,确保在文件读取、加解密过程中处理意外情况。
完整代码实现(含详细注释)
环境准备
确保安装了gmssl
库:
pip install gmssl
AlgorithmTool 类的实现
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT# 工具类:提供SM4加解密以及填充处理
class AlgorithmTool:def __init__(self):pass# 将16进制字符串转换为字节数据@staticmethoddef hex_string_to_bytes(data):return bytes.fromhex(data.replace(" ", ""))# PKCS7填充算法,确保数据长度是16字节的整数倍def pkcs7_padding(self, data):pad_len = 16 - (len(data) % 16) # 计算需要填充的字节数padding = bytes([pad_len] * pad_len) # 生成填充字节return data + padding # 将填充字节附加到数据末尾# 去除PKCS7填充,恢复原始数据def pkcs7_unpadding(self, data):pad_len = data[-1] # 读取最后一个字节,获取填充长度return data[:-pad_len] # 移除填充字节# 使用SM4进行ECB模式加密def encrypt_sm4(self, key, value):crypt_sm4 = CryptSM4()crypt_sm4.set_key(self.hex_string_to_bytes(key), SM4_ENCRYPT) # 设置加密密钥return crypt_sm4.crypt_ecb(value) # 返回加密后的字节数据# 使用SM4进行ECB模式解密def decrypt_sm4(self, key, encrypted_value):crypt_sm4 = CryptSM4()crypt_sm4.set_key(self.hex_string_to_bytes(key), SM4_DECRYPT) # 设置解密密钥decrypted_data = crypt_sm4.crypt_ecb(encrypted_value) # 进行解密return self.pkcs7_unpadding(decrypted_data) # 去除填充,返回解密后的数据
SM4FileHelper 类的实现(含详细注释)
import os
import io# 文件加密辅助类:提供按块加密和解密大文件的功能
class SM4FileHelper:def __init__(self, key):self.ag_tool = AlgorithmTool() # 使用AlgorithmTool处理加解密self.key = key # 16进制密钥self.block_size = 16 * 1024 # 块大小为16的倍数# 加密文件,并在文件开头保存原始文件大小信息def encrypt_file(self, input_file_path, output_file_path):"""按块加密文件内容,并在文件开头存储原始文件大小"""try:# 获取输入文件的总大小file_size = os.path.getsize(input_file_path)# 打开输入和输出文件with open(input_file_path, 'rb') as f_in, open(output_file_path, 'wb') as f_out:# 写入文件大小,便于解密时恢复f_out.write(file_size.to_bytes(8, byteorder='big'))# 循环按块读取文件while True:chunk = f_in.read(self.block_size) # 读取数据块if not chunk:break # 如果没有数据,则结束循环if len(chunk) < self.block_size:# 如果是最后一个块,进行填充后加密padded_chunk = self.ag_tool.pkcs7_padding(chunk)encrypted_chunk = self.ag_tool.encrypt_sm4(self.key, padded_chunk)else:# 如果是完整的块,直接加密encrypted_chunk = self.ag_tool.encrypt_sm4(self.key, chunk)f_out.write(encrypted_chunk) # 写入加密后的数据except IOError as e:print(f"文件操作错误: {e}") # 处理文件读写错误except Exception as e:print(f"加密过程中的错误: {e}") # 处理加密过程中的其他异常# 解密文件,并恢复原始文件内容def decrypt_file(self, input_file_path, output_file_path):"""按块解密文件内容"""try:# 打开输入和输出文件with open(input_file_path, 'rb') as f_in_raw, open(output_file_path, 'wb') as f_out:f_in = io.BufferedReader(f_in_raw)# 读取文件开头的原始文件大小original_file_size = int.from_bytes(f_in.read(8), byteorder='big')decrypted_size = 0 # 已解密的数据大小# 循环按块解密文件while True:encrypted_chunk = f_in.read(self.block_size + 16) # 读取加密数据块if not encrypted_chunk:break # 如果没有数据,则结束循环if f_in.peek(1) == b'': # 判断是否为最后一个块# 最后一个块,进行解密后去填充,并调整大小decrypted_chunk = self.ag_tool.decrypt_sm4(self.key, encrypted_chunk)remaining_size = original_file_size - decrypted_size # 计算剩余未写入的大小decrypted_chunk = decrypted_chunk[:remaining_size] # 截断填充后的多余数据else:# 非最后一个块,直接解密decrypted_chunk = self.ag_tool.decrypt_sm4(self.key, encrypted_chunk)f_out.write(decrypted_chunk) # 写入解密后的数据decrypted_size += len(decrypted_chunk) # 更新已解密数据的大小except IOError as e:print(f"文件操作错误: {e}") # 处理文件读写错误except Exception as e:print(f"解密过程中的错误: {e}") # 处理解密过程中的其他异常# 比较两个文件的大小,用于验证解密文件与原文件是否一致def compare_file_size(self, original_file_path, decrypted_file_path):"""比较两个文件的大小"""try:# 获取原文件和解密后文件的大小original_size = os.path.getsize(original_file_path)decrypted_size = os.path.getsize(decrypted_file_path)# 输出文件大小对比结果print(f"原文件大小: {original_size} 字节")print(f"解密文件大小: {decrypted_size} 字节")# 检查文件大小是否一致if original_size == decrypted_size:print("文件大小一致。")else:print("文件大小不一致!")except IOError as e:print(f"比较文件大小时的错误: {e}") # 处理文件操作异常
测试代码
if __name__ == '__main__':key = '5A3F323272CEA0640C711129E0B6043B' # 您的16进制密钥sm4_helper = SM4FileHelper(key)input_file = 'testbig.jpg' # 要加密的文件encrypted_file = 'encrypted_testbig.sm4' # 保存加密文件decrypted_file = 'decrypted_testbig.png' # 解密后的文件# 加密文件sm4_helper.encrypt_file(input_file, encrypted_file)print(f"文件 {input_file} 已加密保存到 {encrypted_file}")# 解密文件sm4_helper.decrypt_file(encrypted_file, decrypted_file)print(f"文件 {encrypted_file} 已解密保存到 {decrypted_file}")# 比较文件大小sm4_helper.compare_file_size(input_file, decrypted_file)
代码解析与说明
填充与去填充
- PKCS7填充:在
AlgorithmTool
类中实现,用于确保数据长度是块大小的整数倍。 - 填充过程:
- 计算填充字节数
pad_len = 16 - (len(data) % 16)
。 - 生成填充字节
padding = bytes([pad_len] * pad_len)
。 - 将填充字节附加到原数据末尾。
- 计算填充字节数
- 去填充过程:
- 读取最后一个字节,获取填充长度
pad_len = data[-1]
。 - 移除填充字节
data[:-pad_len]
。
- 读取最后一个字节,获取填充长度
按块加密解密逻辑
- 块大小选择:
16 * 1024
字节,满足SM4的块大小为16的倍数,同时平衡处理效率和内存占用。 - 加密逻辑:
- 按块读取文件,检查是否是最后一个块:
- 最后一个块进行填充后加密。
- 非最后一个块直接加密。
- 写入加密后的数据到输出文件。
- 按块读取文件,检查是否是最后一个块:
- 解密逻辑:
- 按块读取加密文件,检查是否是最后一个块:
- 最后一个块解密后去除填充,并根据原始文件大小截断多余字节。
- 非最后一个块直接解密。
- 按块读取加密文件,检查是否是最后一个块:
处理最后一个数据块
- 加密时的填充:
- 只有最后一个数据块需要填充,确保数据长度为16字节的倍数。
- 解密时的去填充:
- 仅对最后一个数据块进行去填充处理,并截断多余字节,确保解密文件与原文件一致。
最佳实践与注意事项
内存优化
- 按块处理:通过分块读取,避免内存占用过高。
- 块大小调整:根据具体情况灵活调整
block_size
。
异常处理
- 文件操作异常:添加
try-except
块,处理文件读写错误。 - 加解密异常:捕获加解密过程中可能的错误,如密钥错误或数据格式问题。
安全性考虑
- 密钥管理:妥善存储和传输加密密钥,避免密钥泄露。
- 敏感信息保护:不要在日志中记录密钥或未加密数据。
总结
通过这次实践,成功解决了SM4加密和解密过程中遇到的问题,深入理解了对称加密算法的细节。正确处理数据块的填充和按块读取处理确保了内存占用的优化和加解密结果的准确性。