AES模块报ValueError因密钥长度不符(仅接受16/24/32字节),须用Scrypt等KDF派生密钥;AES-GCM加密需唯一nonce和salt并正确嵌入文件;解密失败InvalidTag主因key/nonce/salt不匹配或文件损坏。

为什么直接用 AES 模块会报 ValueError: Invalid key size
因为 cryptography 的 AES 类本身不处理密钥派生,它只接受严格长度的密钥(128/192/256 位,即 16/24/32 字节)。你传入字符串或随意长度的 bytes,就会触发这个错误。
正确做法是用 pbkdf2_hmac 或 Scrypt 从密码生成固定长度密钥。生产环境强烈推荐 Scrypt(抗 GPU 暴力破解):
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt from cryptography.hazmat.primitives import hashes import os <p>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/内存成本参数,太小不安全,太大影响性能;默认2**14是较稳妥起点 -
salt必须随密文一起保存(通常前置在加密文件开头),否则无法解密 - 别用
hashlib.sha256(password).digest()这类简单哈希——它不加盐、无迭代、易被彩虹表攻破
如何用 AES-GCM 安全地加密整个二进制文件
AES-GCM 是首选:它同时提供机密性与完整性校验(防篡改),且无需手动管理 IV 和 MAC。但注意:IV(nonce)绝不能复用,且必须唯一。
典型文件加密流程(含 salt + nonce + ciphertext + tag):
立即学习“Python免费学习笔记(深入)”;
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
import os
<p>def encrypt_file(input_path, output_path, key):</p><h1>生成唯一 nonce(12 字节是 GCM 推荐长度)</h1><pre class="brush:php;toolbar:false;">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:
# 先写 salt(如果用了 Scrypt)、nonce、后续再写 tag
f_out.write(nonce) # 实际中这里还应写 salt(若未外置)
while chunk := f_in.read(64 * 1024): # 64KB 块
encrypted_chunk = encryptor.update(chunk)
f_out.write(encrypted_chunk)
# 写入认证标签(必须在最后)
f_out.write(encryptor.finalize())- 不要把
nonce硬编码或用时间戳——os.urandom(12)是唯一可靠方式 - 加密后文件比原文略大:+12 字节(nonce)+16 字节(GCM tag),salt 需额外 +16 字节(若内嵌)
- 切勿在循环里调用
encryptor.finalize()——它只能调用一次,且必须在所有update()之后
解密时遇到 InvalidTag 错误怎么办
InvalidTag 表示密文被篡改、损坏,或解密时用了错误的 key / nonce / salt。它不是“密码错”,而是“数据不可信”的明确信号。
排查顺序必须严格:
- 确认解密用的
key与加密时完全一致(字节级相等,包括空格、换行) - 确认读取的
nonce是加密时写入的前 12 字节(不是跳过、不是多读) - 确认
Scrypt的salt、n、r、p参数与加密时完全相同 - 确认文件没被文本编辑器意外转码(如 Windows 记事本把二进制当 ANSI 打开保存)——务必用
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)
<pre class="brush:php;toolbar:false;">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)
ciphertext = f_in.read() # 注意:tag 在最后 16 字节
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)
实际落地时最容易被忽略的三个细节
一是 salt 和 nonce 的存储位置必须约定清楚。常见做法:加密文件 = [salt(16)][nonce(12)][ciphertext][tag(16)],解密时按此偏移读取。
二是 cryptography 不自动处理文件权限。加密后原文件仍存在,需显式 os.remove() 或安全擦除(如多次覆写)。
三是 Windows 下路径含中文或空格时,open() 不会报错但可能静默失败。建议先用 pathlib.Path(input_path).resolve() 校验路径是否存在且可读。


















