第3章 密钥生成:真随机数生成器与伪随机数生成器

密钥的本质就是随机数。你想想看,如果密钥能被猜出来,加密就形同虚设。所以怎么生成“足够随机”的密钥,是密码学里最基础也最容易翻车的地方。

我个人习惯把随机数生成器分成两类:真随机数生成器(TRNG)和伪随机数生成器(PRNG)。名字听起来有点像,但内核完全不同。我最早做身份认证系统时,觉得用 rand() 就够了,结果测试时同一个密钥反复出现……嗯,从那之后我再也不敢轻视这节内容了。

什么是真随机数生成器(TRNG)

它的核心是:从物理世界里“榨”出不可预测的熵。比如热噪声、大气噪声、鼠标移动轨迹,甚至量子效应。把这些物理信号数字化,再经过后处理,就能得到真正意义上的随机数。

  • 熵源:硬件随机数发生器(如 Intel RDSEED)、专用安全芯片。
  • 输出速率:一般较慢,但不可预测性极强。
  • 应用场景:生成主密钥、初始化向量、证书签名。
我在项目中遇到过:某嵌入式设备采用ADC噪声做熵源,结果温漂导致输出偏斜。解决办法是加一个“冯·诺依曼去偏器”,把01序列中的连续位做异或。小细节,但救过我的命。

什么是伪随机数生成器(PRNG)

说白了,它就是一段确定性的算法。你给它一个种子(seed),它就能源源不断地产出看起来随机的比特串。种子必须是真随机的,否则一切白搭。

常见的PRNG有:

  • 线性同余生成器(LCG)—— 简单,但密码学上不靠谱。
  • 基于哈希函数的生成器(如 SHA-256 构成的 DRBG)—— 符合NIST SP 800-90A 标准。
  • 基于流密码的生成器(如 ChaCha20)—— 速度快,安全。
我曾经查过一个线上服务的密钥泄露事故:他们用Python的 random.randint() 生成会话密钥。那个模块用的是梅森旋转算法(Mersenne Twister),只要得到600多个输出就能反推出种子。结果攻击者花了一天就伪造了管理员会话。血的教训:不要用普通PRNG做密码学安全生成器。

核心对比:TRNG vs. PRNG

维度真随机数生成器(TRNG)伪随机数生成器(PRNG)
熵源物理过程(热噪声、量子等)算法(确定性+种子)
可预测性理论上不可预测只要不知道种子,不可预测
输出速率慢(Kbps级)快(Gbps级)
可重复不能相同种子可重现
密钥用途种子、主密钥会话密钥、随机掩码
典型标准NIST SP 800-90BNIST SP 800-90A
一个常见误解:PRNG因为“伪”字就觉得不安全。其实只要种子够随机、算法够强(比如CTR_DRBG),现代PRNG完全可以用于生产环境。我自己的习惯是:系统启动时用TRNG生成种子,之后所有随机数都由PRNG扩展得到。

实战代码:用Python生成安全随机数

import os
import secrets

# 真随机:从操作系统熵池获取(Linux /dev/urandom)
# 适合做种子、主密钥
key_256bit = os.urandom(32)
print("TRNG密钥(hex):", key_256bit.hex())

# 密码学安全伪随机:Python 3.6+ secrets模块(底层用操作系统熵)
# 适合生成会话密钥、令牌
 = secrets._hex(16)   # 32字节 hex 字符串
session_key = secrets._bytes(32)

# 对比:普通随机(不安全,不要用于密码学)
import random
weak_key = random.randint(0, 2**256)  # 千万别这样用!

为什么 secrets 安全?因为它内部用的是操作系统提供的加密级随机数,在Linux上就是 /dev/urandom。而 random 模块默认种子是当前时间,很容易被暴力。

熵的积累与种子生命周期

TRNG产生的真随机数往往很慢,但在系统中扮演“种子”的角色。系统启动时收集硬件熵(芯片噪声、网络延迟等),存到一个“熵池”里。PRNG从这个池里读取种子,然后快速生成大量随机数。

  1. 采集阶段:系统收集环境噪声,填充熵池。
  2. 提取阶段:PRNG请求种子,从熵池中提取真随机种子。
  3. 扩展阶段:PRNG按算法(如CTR模式)产生密钥流。
  4. 刷新阶段:定期或耗用一定数量后,用新熵重新播种。
我曾经在容器化部署时遇到过熵池枯竭。Docker容器默认共享宿主机的熵源,如果同时启动几十个容器,/dev/random可能阻塞。解决方案:装 haveged 守护进程,它用CPU时序抖动生成熵,虽然不如硬件TRNG强,但足够打破阻塞。当然,生产环境还是建议挂载硬件RNG设备。

知识结构图:TRNG与PRNG的协作关系

TRNG与PRNG协作流程图 物理熵源 热噪声/量子/鼠标 TRNG 后处理 去偏·哈希·抽提 熵池 Linux /dev/urandom PRNG 引擎 CTR_DRBG/ChaCha20 密钥输出 会话密钥·令牌·IV 种子只能从TRNG取 PRNG永远不能自产熵

避坑清单:密钥生成时的常见失误

  • 直接用时间戳当种子:我在渗透测试时,用 time(0) 作为种子的场景十有八九都能在几秒内撞到。
  • 重复使用种子:容器重启后种子不变,导致所有容器的密钥相同。
  • 忽略熵池告警:如果 /proc/sys/kernel/random/entropy_avail 长期低于100,说明系统可能“饿死”,需要人工补充熵源。
  • 在虚拟化环境误用 /dev/random:它可能会阻塞直到有足够熵,但虚拟机通常没有物理熵源,推荐用 /dev/urandom 或 getrandom() 系统调用。
我曾经帮一个金融项目做代码审计,发现他们用 UUID.randomUUID() 生成会话密钥。UUID虽然是随机的,但它的随机性来源是系统默认的伪随机数生成器(Java中默认是SHA1PRNG,但老版本可能用LCG),且版本1包含MAC地址和时间戳。攻击者只要能拿到一个UUID就可以缩小搜索空间。记住:UUID不是密码学安全的随机数。

小结要点

  • TRNG提供不可预测的“种子”,PRNG基于种子高速扩展。
  • 密码学中只使用“密码学安全伪随机数生成器”(CSPRNG)。
  • 操作系统提供的 /dev/urandom 和 getrandom() 已经够用,不要自己造轮子。
  • 密钥生成后,种子必须妥善销毁,防止内存雷劈泄露。