Python编程:大文件Hash计算及加解密

一.前言

在互联网时代,无论文件存储在本地端还是云端,安全性问题都是不容忽略的重要考虑因素。尤其对于重要的学习资料,我们可以在存储前对其进行加密处理,并在需要时使用解密进行恢复。然而,面对大文件,比如约4GB的文件,一次性进行Hash计算及加解密似乎存在一些难题,可能导致内存溢出等问题。因此,设计一种高效的大文件Hash计算及加解密流程显得尤为重要。

二.实现

先简单了解下本文涉及的两种安全算法:

SHA-256

加密哈希函数,设计用于产生固定长度的哈希值,通常用于确保数据的完整性。分组大小为64字节。

AES/CBC/PKCS5Padding

对称密钥加密算法,被广泛用于保护数据的机密性。CBC模式使用前一个块的密文与当前块的明文进行异或操作,增强了安全性。需要一个初始化向量(IV)来加密第一个块。分组大小为16字节,不足时需要进行填充。

具体流程

本文中使用SHA-256来计算文件的Hash来确保数据的完整性,使用AES/CBC/PKCS5Padding对文件进行加解密。

对文件进行分块时,要选择合适的块大小BLOCK_SIZE64B的倍数,本文选取2MB,选择过小,AES填充会使加密后的文件体积增加较大。

需要注意的是,对16B倍数的块,AES填充会增加AES.block_size数据,因此加密时BLOCK_SIZE读取文件,解密需要BLOCK_SIZE+AES.block_size读取文件。

具体代码实现如下,本人电脑配置加解密4GB大小文件均耗时10s左右。

pip install pycryptodome
from hashlib import sha256
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from time import time
from datetime import datetime
import json
import os

# ASE:16B
# SHA256:64B
# 加密块大小 2MB
BLOCK_SIZE = 1024 * 1024 * 2


def log(text: str) -> None:
    """
    日志输出
    """
    format = "%Y-%m-%d %H:%M:%S"
    format_text = f"[{datetime.now().strftime(format)}] {text}"
    print(format_text)


def simple_aes_decrypt(key: str, data: bytes) -> bytes:
    """
    简单AES/CBC/PKCS5Padding解密
    """
    # 创建cipher对象
    cipher = AES.new(key=key.encode(), mode=AES.MODE_CBC, iv=key.encode())
    # 对输入数据进行解密
    decrypted_data = cipher.decrypt(data)
    return unpad(decrypted_data, AES.block_size)


def simple_aes_encrypt(key: str, data: bytes) -> bytes:
    """
    简单AES/CBC/PKCS5Padding加密
    """
    # 创建cipher对象
    cipher = AES.new(key=key.encode(), mode=AES.MODE_CBC, iv=key.encode())
    # 对输入数据进行加密
    encrypted_data = cipher.encrypt(pad(data, AES.block_size))
    return encrypted_data


def encrypt_file(file_path: str, key: str, output_name: str = None) -> str:
    """
    AES/CBC/PKCS5Padding加密文件
    """
    # 检查文件是否存在
    if not os.path.exists(file_path):
        raise FileNotFoundError("文件不存在")

    # 文件属性
    file_input = open(file_path, "rb")
    file_input_name = os.path.basename(file_path)
    file_input_size = os.path.getsize(file_path)
    # 如果没有指定输出文件名,则使用20位Hash值作为文件名
    if not output_name:
        output_name = sha256(file_input_name.encode()).hexdigest()[:20]
    file_output = open(output_name, "wb")

    # 同时进行Hash计算和AES加密
    log("开始加密...")
    cipher_hash = sha256()
    cipher_encrypt = AES.new(key=key.encode(), mode=AES.MODE_CBC, iv=key.encode())
    while True:
        bytes = file_input.read(BLOCK_SIZE)
        if not bytes:
            break
        cipher_hash.update(bytes)
        # size = BLOCK_SIZE+AES.block_size
        encrypt_bytes = cipher_encrypt.encrypt(pad(bytes, AES.block_size))
        file_output.write(encrypt_bytes)
    file_input.close()
    file_output.close()
    hash = cipher_hash.hexdigest()

    # 保存参数,用于解密时恢复文件名、完整性验证
    config = {
        "filename": file_input_name,
        "size": file_input_size,
        "hash": hash,
        "time": int(time() * 1000),
    }
    config = json.dumps(config, ensure_ascii=False)
    config = simple_aes_encrypt(key, config.encode())
    file_config_name = output_name + ".config"
    file_config = open(file_config_name, "wb")
    file_config.write(config)
    file_config.close()
    log("加密完成")

    return output_name


def decrypt_file(file_path: str, key: str, output_name: str = None) -> None:
    """
    AES/CBC/PKCS5Padding解密文件
    """
    # 检查文件是否存在
    if not os.path.exists(file_path):
        raise FileNotFoundError("文件不存在")

    # 检查配置文件是否存在
    config = None
    file_config_name = file_path + ".config"
    if not os.path.exists(file_config_name):
        log("配置文件不存在")
    else:
        file_config = open(file_config_name, "rb")
        config = file_config.read()
        file_config.close()
        config = simple_aes_decrypt(key, config)
        config = json.loads(config.decode())
        if not output_name:
            output_name = config["filename"]

    file_output = open(output_name, "wb")
    file_input = open(file_path, "rb")

    # 同时进行Hash计算和AES加密
    log("开始解密...")
    cipher_hash = sha256()
    cipher_decrypt = AES.new(key=key.encode(), mode=AES.MODE_CBC, iv=key.encode())
    while True:
        # size = BLOCK_SIZE+AES.block_size
        bytes = file_input.read(BLOCK_SIZE + AES.block_size)
        if not bytes:
            break
        decrypt_bytes = unpad(cipher_decrypt.decrypt(bytes), AES.block_size)
        cipher_hash.update(decrypt_bytes)
        file_output.write(decrypt_bytes)

    file_input.close()
    file_output.close()
    log("解密完成")

    # 验证文件完整性
    if config:
        hash = cipher_hash.hexdigest()
        if hash != config["hash"]:
            raise Exception("文件完整性验证失败")
        else:
            log("文件完整性验证成功")
    else:
        log("缺少配置文件,未验证文件完整性")

    return output_name


if __name__ == "__main__":
    file_path = r"C:\Users\xxxx\Downloads\xxxxx.zip"
    key = "1234567890123456" # 16字节/32字节
    output_name = encrypt_file(file_path, key)
    decrypt_file(output_name, key)

三.总结

本文讲述了一种大文件Hash计算及加解密流程,可以根据实际需要替换其中算法和优化流程,完善成命令行工具应在日常中使用。