PBKDF2 + AES-GCM 是本地加密存储(钱包 vault、配置文件保护)的标准方案:密码经 PBKDF2 派生出密钥,AES-GCM 提供加密和完整性验证。
加密流程
Password
↓
PBKDF2-HMAC-SHA512(iterations=600000, salt=随机16字节)
↓
32 字节 AES Key
↓
AES-256-GCM(nonce=随机16字节)
↓
Ciphertext + Tag
安装依赖
pip install pycryptodome
完整实现
import json
import os
import base64
from hashlib import pbkdf2_hmac
from Crypto.Cipher import AES
PBKDF2_ITERATIONS = 600_000 # NIST 2023 推荐最低值
def derive_key(password: str, salt: bytes) -> bytes:
return pbkdf2_hmac(
"sha512",
password.encode("utf-8"),
salt,
PBKDF2_ITERATIONS,
dklen=32, # AES-256
)
def encrypt(password: str, plaintext: str) -> dict:
salt = os.urandom(16)
nonce = os.urandom(16)
key = derive_key(password, salt)
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
ciphertext, tag = cipher.encrypt_and_digest(plaintext.encode("utf-8"))
return {
"salt": base64.b64encode(salt).decode(),
"nonce": base64.b64encode(nonce).decode(),
"ciphertext": base64.b64encode(ciphertext).decode(),
"tag": base64.b64encode(tag).decode(),
}
def decrypt(password: str, vault: dict) -> str:
salt = base64.b64decode(vault["salt"])
nonce = base64.b64decode(vault["nonce"])
ciphertext = base64.b64decode(vault["ciphertext"])
tag = base64.b64decode(vault["tag"])
key = derive_key(password, salt)
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
plaintext = cipher.decrypt_and_verify(ciphertext, tag)
return plaintext.decode("utf-8")
使用示例
# 加密
secret = '{"privateKey": "0xabcd..."}'
vault = encrypt("my_strong_password", secret)
# 存储为 JSON
with open("vault.json", "w") as f:
json.dump(vault, f, indent=2)
# 解密
with open("vault.json") as f:
vault = json.load(f)
plaintext = decrypt("my_strong_password", vault)
print(plaintext)
vault.json 格式:
{
"salt": "base64...",
"nonce": "base64...",
"ciphertext": "base64...",
"tag": "base64..."
}
为什么选 AES-GCM 而非 CBC
| 模式 | 加密 | 完整性验证 | 推荐 |
|---|---|---|---|
| AES-CBC | ✅ | ❌(需额外 HMAC) | 不推荐新项目 |
| AES-GCM | ✅ | ✅(内置 Tag) | ✅ 推��� |
| AES-CTR | ✅ | ❌ | 需配合 HMAC |
GCM 模式同时提供加密和认证,decrypt_and_verify 会在解密前验证 Tag,密文被篡改时抛出 ValueError。
关键参数说明
PBKDF2 迭代次数:越高越安全,NIST 2023 建议 SHA-512 至少 210,000 次,MetaMask 等钱包用 600,000 次。代价是派生速度变慢(约 0.5~2 秒),对正常用户不感知,但让暴力破解成本提高数十万倍。
salt 唯一性:每次加密生成新的随机 salt,防止相同密码产生相同密钥(彩虹表攻击)。salt 不需要保密,公开存储即可。
nonce 唯一性:GCM 的 nonce 绝对不能重用于同一密钥,否则 GCM 的安全性完全崩溃。每次加密随机生成是最安全的做法。
密码错误时的行为
try:
plaintext = decrypt("wrong_password", vault)
except ValueError:
print("密码错误或数据被篡改")
decrypt_and_verify 验证 Tag 失败时抛出 ValueError,不会泄漏任何明文信息。
