Python 中密码生成的安全性:random 与 secrets 模块对比
密码的质量由熵值定义,这并非字符串本身的属性,而是生成过程的特征。公式 H = L × log₂(N) 假设每个字符从大小为 N 的字符集中独立且等概率地选择,长度为 L。对于 8 个小写字母(N=26):约 37.6 位。对于完整 ASCII 字符集(N=94):约 79 位。
一个常见的误解是:密码表面上的随机性保证了其强度。像 f9A$kL2pQzX1 这样的两个密码可能具有相同的理论熵值,但根据所用生成器的不同,实际安全性差异巨大。
random 模块:确定性伪随机数生成器
random 模块实现了梅森旋转算法(MT19937),内部状态约为 19937 位。每个值都基于前一个值确定性地生成。默认种子取自 os.urandom(),但 PRNG 的固有特性依然存在:
- 确定性:一个种子产生一个可预测的序列。
- 可恢复性:状态可以从约 624 个原始 32 位输出中重建。
- 缺乏加密安全性:无法抵抗状态恢复攻击。
典型代码示例:
import random
import string
alphabet = string.ascii_letters + string.digits
def generate_password(length=12):
return ''.join(random.choice(alphabet) for _ in range(length))
密码是 PRNG 序列的一个片段。如果状态已知,序列就是可预测的。
secrets 模块:直接访问系统熵源
Secrets 直接使用 os.urandom() 进行每次调用:
- secrets.randbelow(n) → os.urandom()。
- random.SystemRandom() → os.urandom()。
没有固定状态,没有确定性。每个字节都来自硬件源:中断、网络噪声、鼠标移动。
差异至关重要:random 制造安全假象,secrets 提供真正安全。
实际攻击:恢复种子和状态
实验:从 GigaChat 获取 5 个连续密码(长度 12,Python 3.9.18,时间 2026 年 3 月 24 日 11:11):
假设:种子 = 服务器时间戳 ±1 小时。
from datetime import datetime, timedelta
base_time = datetime(2026, 3, 24, 11, 11, 0)
window_seconds = 3600
for offset in range(-window_seconds, window_seconds):
t = base_time + timedelta(seconds=offset)
seed = int(t.timestamp())
# 初始化 random.seed(seed) 并验证
结果:无匹配。使用 os.urandom() 作为种子可抵御基于时间的暴力攻击。
对 MT19937 的真实攻击需要 624 个原始值。密码是扭曲的输出(选择 → 字符),5 个密码总共只有 60 次选择。恢复是不可能的。
攻击边界:
- 需要原始 PRNG 输出。
- 可预测的种子(时间、PID)。
- 重复使用同一生成器。
- 已知算法。
关键要点
- 熵是过程的属性,而非结果:始终验证您的生成器。
- random(梅森旋转算法)在给定足够数据时易受状态恢复攻击。
- secrets/os.urandom 是密码、密钥和令牌的唯一选择。
- random 的随机性假象在生产环境中是危险的。
- PRNG 攻击仅在种子错误或状态泄露时现实可行。
对中高级开发者的建议
在代码中实现:
import secrets
import string
alphabet = string.ascii_letters + string.digits + string.punctuation
def secure_password(length=16):
return ''.join(secrets.choice(alphabet) for _ in range(length))
避免:
- 使用 random 进行身份验证。
- 固定种子。
- 重用生成器。
测试:生成 10⁶ 个密码,检查均匀分布(卡方检验)。
— Editorial Team
暂无评论。