发布于2026-07-20 阅读(0)
扫一扫,手机访问
在Python中处理文件加密,尤其是用到AES这类对称加密算法时,很多开发者都遇到过“ValueError: Invalid key size”这个拦路虎。其实,这背后往往是密钥长度管理不当,或者对加密流程的理解不够清晰。归根结底,AES本身只接受三种固定长度的密钥:16字节、24字节或32字节。如果你传进去一个“HelloWorld”这种长度不对的字符串,或者一个随意生成的字节串,它自然会报错。
解决这个问题的关键,不在于硬编码一个16字节的密钥,而在于使用一个可靠的密钥派生函数(KDF)。换句话说,你可以由用户输入一个“密码”,然后通过算法把这个密码转化成符合AES要求的固定长度密钥。在生产环境中,Scrypt算法是当前对抗GPU暴力破解的强力武器,也是推荐的做法。它的核心逻辑就像下面这样:
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt from cryptography.hazmat.primitives import hashes import os password = b"my_secret_pass" salt = os.urandom(16) # 每次加密必须用新 salt kdf = Scrypt(salt=salt, length=32, n=2**14, r=8, p=1) key = kdf.derive(password)
n=2**14是CPU/内存成本参数,太低不安全,太高影响性能,这个值算是一个比较稳妥的起点。salt(盐值)必须随密文一起保存,通常放在加密文件的开头。否则,解密时没有盐值,无法重新生成密钥,你就永远打不开文件了。hashlib.sha256(password).digest()来生成密钥。这种做法不加盐、无迭代,容易被彩虹表一锅端,绝对要避免。确定了密钥,下一步就是加密方式。AES-GCM是当前的首选方案,因为它不仅提供加密,还自带完整性校验,能有效防止密文被篡改。不过,使用GCM模式有一个绝对不能碰的红线:它的初始化向量(IV,在GCM里也叫nonce)必须是唯一的,绝对不能重复使用。
一个典型的文件加密流程,会包含盐值、nonce、密文以及认证标签。下面这个代码片段展示了这个过程:
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
import os
def encrypt_file(input_path, output_path, key):
# 生成唯一 nonce(12 字节是 GCM 推荐长度)
nonce = os.urandom(12)
# 构建 cipher 并获取加密器
cipher = Cipher(algorithms.AES(key), modes.GCM(nonce))
encryptor = cipher.encryptor()
# 读取原文件并分块加密(避免大文件爆内存)
with open(input_path, "rb") as f_in, open(output_path, "wb") as f_out:
# 先写 nonce(实际中这里还应写 salt,若未外置)
f_out.write(nonce)
while chunk := f_in.read(64 * 1024): # 64KB 块
encrypted_chunk = encryptor.update(chunk)
f_out.write(encrypted_chunk)
# 写入认证标签(必须在最后)
f_out.write(encryptor.finalize())
os.urandom(12)是唯一可靠的随机来源。encryptor.finalize()只能调用一次,而且必须在所有update()操作之后,绝不能放在循环里。解密时遇到InvalidTag错误,是很多新手最头疼的问题。这个错误信息本身就告诉我们:密文不可信了。它意味着数据可能被篡改、损坏,或者解密时使用的密钥、nonce、盐值不匹配。
排查这个问题,需要遵循严格的顺序:
rb和wb模式。下面是解密流程的关键代码片段:
def decrypt_file(input_path, output_path, password, salt):
kdf = Scrypt(salt=salt, length=32, n=2**14, r=8, p=1)
key = kdf.derive(password)
with open(input_path, "rb") as f_in:
nonce = f_in.read(12) # 严格读 12 字节
cipher = Cipher(algorithms.AES(key), modes.GCM(nonce))
decryptor = cipher.decryptor()
# 读剩余全部为密文,注意 tag 在最后 16 字节
ciphertext = f_in.read()
tag = ciphertext[-16:]
ciphertext = ciphertext[:-16]
try:
plaintext = decryptor.update(ciphertext) + decryptor.finalize_with_tag(tag)
except Exception as e:
raise ValueError("Decryption failed: InvalidTag or bad key") from e
with open(output_path, "wb") as f_out:
f_out.write(plaintext)
理论与代码都写好了,但在实际项目中,有几个细节常常会让人抓狂。
第一,盐值和nonce的存储位置必须清晰约定。一个常见的做法是采用“文件头”结构:[salt(16)][nonce(12)][ciphertext][tag(16)]。解密时,必须严格按照这个偏移量来读取,否则拿到的数据全是错的。
第二,cryptography库本身不会自动处理文件权限。加密操作完成后,原始文件依然存在,你需要手动通过os.remove()将其删除,或者采用更安全的擦除方法,比如多次覆写。
第三,在Windows环境下,如果文件路径包含中文或空格,虽然open()函数可能不会立即报错,但有可能会在后续环节静默失败。为了保险起见,建议在操作前先使用pathlib.Path(input_path).resolve()来校验路径是否存在且可读,确保万无一失。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8