返回首页

Python 密码生成:使用 secrets 而非 random

本文分解了为什么 random 模块不适合密码生成,因为 Mersenne Twister 确定性。与 secrets 比较、熵分析和失败的种子攻击尝试。安全代码推荐。

为什么 Python 中的 random 对密码危险:secrets 实战
Advertisement 728x90

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 的固有特性依然存在:

Google AdInline article slot
  • 确定性:一个种子产生一个可预测的序列。
  • 可恢复性:状态可以从约 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() 进行每次调用:

Google AdInline article slot
  • secrets.randbelow(n) → os.urandom()。
  • random.SystemRandom() → os.urandom()。

没有固定状态,没有确定性。每个字节都来自硬件源:中断、网络噪声、鼠标移动。

差异至关重要:random 制造安全假象,secrets 提供真正安全。

实际攻击:恢复种子和状态

实验:从 GigaChat 获取 5 个连续密码(长度 12,Python 3.9.18,时间 2026 年 3 月 24 日 11:11):

Google AdInline article slot

假设:种子 = 服务器时间戳 ±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

Advertisement 728x90

继续阅读