应使用secrets模块而非random生成密码,因random基于可预测的伪随机数生成器,而secrets直接调用操作系统密码学安全熵源;推荐用secrets.choice()配合string.ascii_letters、digits和符号集生成16位强密码,并用cryptography.Fernet对密钥进行安全加密存储。

用 secrets 模块生成真正安全的随机密码
别用 random——它不适用于密码场景,random 基于伪随机数生成器(PRNG),可被预测;而 secrets 专为密码学安全设计,直接读取操作系统熵源(如 /dev/urandom)。生成 16 位含大小写字母、数字、符号的密码,推荐写法:
import secrets import string <p>def generate_password(length=16): chars = string.ascii<em>letters + string.digits + "!@#$%^&*" return ''.join(secrets.choice(chars) for </em> in range(length))</p><p>pwd = generate_password() # 例如:'K9#vQx$mL2pRz!W7'
注意:secrets.choice() 比 secrets.randbelow() + 索引更简洁安全;避免手动拼接字符集时漏掉转义字符(如 \),"!@#$%^&*" 已足够覆盖常见需求且无歧义。
把密码存进本地 SQLite 加密库(用 sqlite3 + cryptography)
SQLite 本身不加密,必须自己加解密层。推荐用 cryptography 的 Fernet 对称加密(需先用 fernet.generate_key() 创建并**安全保管主密钥**):
立即学习“Python免费学习笔记(深入)”;
from cryptography.fernet import Fernet
import sqlite3
import json
<h1>假设 key 已存在且已加载(绝不能硬编码在脚本里!)</h1><p>key = b'...' # 来自环境变量或独立密钥文件
cipher = Fernet(key)</p><p>conn = sqlite3.connect('vault.db')
conn.execute('CREATE TABLE IF NOT EXISTS passwords (id INTEGER PRIMARY KEY, site TEXT, encrypted_pwd BLOB)')</p><p>def save_password(site: str, plain_pwd: str):
encrypted = cipher.encrypt(plain_pwd.encode())
conn.execute('INSERT INTO passwords (site, encrypted_pwd) VALUES (?, ?)', (site, encrypted))
conn.commit()关键点:
-
encrypted_pwd字段必须是BLOB类型,不是TEXT - 每次
save_password()后必须调用conn.commit(),否则不落盘 - 主密钥
key绝不能出现在代码或 Git 中——应通过os.getenv("VAULT_KEY")或读取~/.vault.key(权限设为600)获取
运行时提示用户输入主密钥而不是写死在脚本里
硬编码密钥等于裸奔。最简可行做法是首次运行时生成密钥并提示保存,后续运行时要求输入:
import getpass
from cryptography.fernet import Fernet
<p>def load_or_create_key():
try:
with open('.vault.key', 'rb') as f:
return f.read()
except FileNotFoundError:
print("首次使用:正在生成主密钥...")
key = Fernet.generate_key()
with open('.vault.key', 'wb') as f:
f.write(key)
print("✅ 主密钥已保存至 .vault.key,请立即备份并设置权限:chmod 600 .vault.key")
return key</p><h1>实际使用前验证密钥有效性</h1><p>try:
cipher = Fernet(load_or_create_key())
except ValueError:
print("❌ 主密钥格式错误,请检查 .vault.key 内容是否被篡改")
exit(1)注意:getpass.getpass() 不适合这里——Fernet 密钥是 32 字节 base64 编码,人类无法可靠输入;所以必须用文件方式管理,且该文件本身要靠系统权限保护。
查询密码时解密失败的常见原因和修复
执行 cipher.decrypt() 报 cryptography.fernet.InvalidToken 是高频错误,基本就三类情况:
- 用了错误的密钥(比如换机器没同步
.vault.key) - 数据库里存的是未加密明文(字段类型错设成
TEXT,或插入时忘了调用cipher.encrypt()) - 读取
BLOB后误用.decode('utf-8')再传给decrypt()(decrypt()要求原始bytes)
安全解密示例:
def get_password(site: str) -> str:
row = conn.execute('SELECT encrypted_pwd FROM passwords WHERE site = ?', (site,)).fetchone()
if not row:
raise KeyError(f"未找到站点 {site}")
encrypted = row[0] # 是 bytes,不是 str
return cipher.decrypt(encrypted).decode('utf-8')真正麻烦的从来不是生成密码,而是密钥生命周期管理——一旦 .vault.key 丢失,所有密码永久不可恢复。


















