随机数生成与熵源:安全随机数生成器、真随机与伪随机

说到密钥,第一个绕不开的就是随机数。你想想看,如果生成密钥的随机数不够「随机」,那这把锁说白了就是纸糊的。我刚入行那会儿,遇到过有人直接用 C 语言的 rand() 生成 RSA 密钥,结果呢?同一台机器每次启动后前几个密钥一模一样——因为种子是固定的。嗯,这个坑我后面会细讲。

今天咱们就把这块掰开揉碎了聊。先搞清楚什么是真随机,什么是伪随机,再来看安全随机数生成器(CSPRNG)到底怎么用。

真随机 vs 伪随机:本质区别

很多人觉得「随机就是随便来个数」,其实差远了。真随机数来源于物理世界的不可预测事件,比如热噪声、放射性衰变、鼠标移动轨迹。伪随机数则是通过算法从种子值计算出来的序列,只要种子一样,序列就完全一样。

对比项真随机数 (TRNG)伪随机数 (PRNG)
来源物理熵源算法 + 种子
可预测性理论上不可预测已知种子可完全预测
速度慢(通常几十 KB/s)极快(GB/s 级别)
用途种子、密钥生成、一次性随机模拟、游戏、非安全场景
安全要求必须用于密钥仅用于非安全场景

其实真随机和伪随机不是非此即彼的关系。大多数安全系统用的是混合方式:先用真随机数作为种子,再用伪随机算法快速扩展出大量随机数。只要种子足够熵,结果就是安全的。

安全随机数生成器(CSPRNG)

CSPRNG 全称是 Cryptographically Secure Pseudo-Random Number Generator。它跟普通 PRNG 最大的区别在于——它必须扛住「即使攻击者知道算法,不知道种子,就无法预测任何输出」。业内常用的 CSPRNG 有:

  • /dev/urandom(Linux/BSD)—— 一旦初始化,永不阻塞,熵池不够时会退化为伪随机
  • getrandom()(Linux 3.17+)—— 比 /dev/urandom 更可控,可指定是否阻塞等待熵
  • CryptGenRandom(Windows)—— 微软的 CSPRNG,但早期实现有过 bug
  • 操作系统提供的 API(如 arc4random、SecureRandom)
我的个人习惯:在 Linux 上一直用 getrandom() 且不设 GRND_NONBLOCK,让它启动时等等熵。虽然第一次调用可能卡个几毫秒,但种子质量有保障。

熵源与熵估计

熵源是随机数的「原材料」。常见的熵源有:

  1. 硬件噪声:CPU 内部的热噪声、时钟抖动、磁盘寻道时间波动
  2. 用户交互:鼠标移动、键盘敲击间隔(服务器上很难获取)
  3. 网络包到达间隔:不依赖硬件,但容易被攻击者观察
  4. 专用硬件:TPM 芯片、Intel RDRAND 指令、熵源芯片

熵估计是个玄学。你没法精确知道一个事件里到底有多少「不可预测性」。内核通常用保守估计,比如每次键盘中断算半个比特熵。我在项目中见过一个嵌入式设备,因为熵池长期处于「欠熵」状态,导致 openssl 生成密钥时卡死 10 多秒——嗯,后来加了硬件熵源才解决。

实践:如何获取高质量随机数

写代码的时候别自己实现随机数生成器。用现成的、经过审计的 API:

# Python 3.6+ 标准做法
import secrets

# 生成 32 字节真随机数(需要足够熵)
key = secrets._bytes(32)

# 生成十六进制密钥字符串
secret_key = secrets._hex(16)
print(secret_key)  # 每次都不同
千万别用 random 模块! 我见过一个项目用 random.randint 生成 AES 密钥,结果被逆向出种子后,所有加密数据都变成了裸奔。 random 的算法是 Mersenne Twister,专门为模拟设计,不是 CSPRNG。

如果是在 Linux 上直接用 C 写底层代码,推荐用 getrandom 系统调用:

#include <sys/random.h>
#include <stdio.h>

unsigned char buf[32];
if (getrandom(buf, sizeof(buf), 0) == sizeof(buf)) {
    // buf 里是安全的随机数
    printf("随机数生成成功\n");
}

一次我个人踩过的坑

几年前我维护一个 IoT 网关,由于硬件没有 TRNG,就用 /dev/urandom 生成密钥。一切正常。直到有一次系统刚启动,内核熵池还没积累够,就调用了 /dev/urandom——你猜怎么着?生成的 SSH 主机密钥跟另一台设备一模一样!因为两个设备的启动序列、时间戳、硬件都一样,初始熵池几乎为零,/dev/urandom 退化成确定性输出。从那以后,我对「熵初始化」特别敏感。哪怕多等 100ms,也要确保种子里面混进了真正的物理噪声。

知识体系:随机数与密钥边界

随机数生成与熵源知识体系 硬件噪声 用户输入 系统事件 专用硬件(TRNG) 熵源 收集 熵池 (内核/用户空间) 去随机化 CSPRNG (如 /dev/urandom) 密钥生成 种子 普通 PRNG 不可用于密钥 核心原则 熵源 → 熵池 → CSPRNG → 密钥 缺一不可,避免使用普通PRNG

总结一句话:真随机负责「播种」,CSPRNG 负责「产出」。安全底线是永远不要用普通 PRNG 生成密钥、随机数种子、IV 或随机盐值。如果平台支持,直接用操作系统提供的安全随机接口。