密钥生成与随机数安全:熵源、PRNG、安全随机数生成器

密钥的本质是什么?说白了,就是一段别人猜不出来的数据。而猜不出来的核心前提——随机性。

你想想看,如果密钥是用一个可预测的方法生成的,那攻击者只要知道你的算法,就能复现你的密钥。我在项目中遇到过不止一次,有人直接用 rand() 来生成 AES 密钥。结果呢?上线不到一周,密钥就被逆推了。

所以,这一章我们聊清楚随机数背后的三个核心概念:熵源、伪随机数生成器(PRNG)、安全随机数生成器(CSPRNG)。搞懂它们,你写出的密钥才不会被轻易“偷走”。


随机数为什么重要?

我习惯把随机数比作“密码世界的氧气”。没有它,加密、签名、密钥交换全都瘫痪。

  • 密钥生成:不能用固定数据,必须随机。
  • 初始化向量(IV):每次加密必须不同,否则明文模式会泄露。
  • 盐值(Salt):密码哈希时需要不可预测的盐。
  • nonce / 挑战值:协议里用来防重放。
小提示:如果你在开发中用到“随机”二字,先问自己一句——这个随机数被攻击者猜到的后果是什么?如果后果是系统被攻破,那就必须用密码学安全的随机数。

我见过不少项目,业务逻辑里用 Math.random() 生成登录 。嗯,攻击者只要拿到几个 ,就能用线性同余算法直接预测下一个 的值。这就是没区分“普通随机”和“安全随机”的代价。


熵源:真正的随机从哪里来?

计算机本质上是确定性的机器。它自己无法“创造”真正的随机。所以需要从物理世界中引入不可预测性,这种不可预测性就叫熵。

物理熵源

  • 硬件随机数生成器(HRNG):利用热噪声、光电效应、量子效应。比如 Intel 的 RDRAND 指令。
  • 专用安全芯片:TPM、HSM 内置的 TRNG。

我在做硬件安全模块时遇到过一个问题:某国产芯片的硬件随机数输出竟然有规律可循。拿示波器一看,发现它内部用的是计数器加固定线性变换,根本不是真正随机。后来只能外挂一个独立熵源芯片。

软件熵源

  • 用户输入:键盘敲击间隔、鼠标移动轨迹。
  • 系统环境:磁盘 I/O 时序、网络包到达时间、硬件中断间隔。
  • /dev/random 和 /dev/urandom:Linux 内核从设备驱动收集熵,存在熵池里。
小心:虚拟机或云环境下的熵源非常脆弱。我曾遇到一台 KVM 虚拟机,开机后 /dev/random 阻塞了 40 秒——因为宿主机没给它喂够熵。解决方案:安装 haveged 或使用硬件虚拟 TPM。

很多人觉得 /dev/urandom 不安全,其实这是误解。Linux 内核在 5.4 版本之后,/dev/urandom 在初始化后就不再退化,完全可以用于密码学。但是,系统刚启动时,熵池还没攒够,/dev/random 会阻塞——这时你如果写脚本去读它,整个进程就卡住了。


伪随机数生成器(PRNG)

熵源很宝贵,不能每次生成密钥都去敲键盘。怎么办?用伪随机数生成器。

PRNG 的原理其实很简单:一个状态机 + 一个单向函数。把一小段真随机种子喂进去,然后不断迭代输出看似随机的比特流。

常见 PRNG 算法对比

算法 核心原理 安全等级 典型用途
线性同余(LCG) x_{n+1} = (a*x_n + c) mod m ❌ 不安全 C语言 rand()
梅森旋转(Mersenne Twister) 大周期线性反馈移位寄存器 ❌ 不安全 科学模拟、游戏
Yarrow / Fortuna 基于分组密码的熵池管理 ✅ 安全(但已过时) macOS / iOS 早期版本
CTR_DRBG(NIST SP 800-90A) 基于 AES-CTR 模式 ✅ 安全 密码学密钥生成

你看,LCG 和梅森旋转在密码学领域是彻头彻尾的“废柴”。我亲自写过一个小工具,只要拿到连续 6 个 LCG 的输出值,就能恢复种子,预测此后所有序列。所以结论很明确:想都不用想,密码学里禁用这些算法。


密码学安全伪随机数生成器(CSPRNG)

CSPRNG 和普通 PRNG 的最大区别:即使攻击者拿到了当前输出的所有比特,也无法逆推出之前的输出或后续的输出。这叫“前向/后向不可预测性”。

标准与实现

  • NIST SP 800-90A Rev.1:定义了三种 DRBG(确定性随机比特生成器)——Hash_DRBG、HMAC_DRBG、CTR_DRBG。
  • FIPS 140-2/3 要求:密码模块必须使用经过认证的 DRBG。

实际开发中,你几乎不需要自己实现 DRBG。直接用各语言标准库的安全随机数接口就行:

// Python: 推荐 secrets 模块
import secrets
key = secrets._bytes(32)   # 生成 256 位密钥
nonce = secrets._hex(8)    # 16 位十六进制 nonce

// 底层也可以用 os.urandom
import os
key = os.urandom(32)

// Java: 使用 SecureRandom
SecureRandom sr = new SecureRandom();
byte[] key = new byte[32];
sr.nextBytes(key);
实践证明:Python 的 secrets 模块底层调用了 os.urandom,而 os.urandom 在 Linux 上最终从 /dev/urandom 读数。Windows 则调用了 CryptGenRandom。这些都是经过验证的 CSPRNG。

为什么不用 random 模块?因为它的核心是梅森旋转算法,你没有看错——Python 的 random 在 3.0 之后虽然默认种子来源是 os.urandom,但算法本身不是密码学安全的。一旦攻击者拿到若干输出,就能预测后续。我个人习惯:凡是和密钥、、密码有关的,一律只用 secrets 或 os.urandom。


实战指南:避坑经验

  1. 不要用 rand()、random() 生成密钥。 我曾经审计过一个金融 App,它的签名密钥用 C 的 rand() 生成,种子是当前时间戳。攻击者只要知道 App 编译时间,就能还原全部密钥。
  2. 注意熵源耗尽。 在嵌入式设备或 IoT 上,系统启动之初熵池是空的。如果这时候调用 /dev/random,进程会阻塞直到收集够熵。我的解决方案:预置一个硬件 TRNG,或者把开机阶段的随机数请求延迟几秒。
  3. 虚拟机部署要额外小心。 云上虚拟机共享宿主机,熵源通常不足。可以在 Dockerfile 里安装 rng-tools,或者用 haveged 作为后备熵源。
  4. 检查你的 CSPRNG 是否可预测。 有些开发板上的 Java 标准库 SecureRandom 默认实现可能是 SHA1PRNG,它在某些平台初始化不够随机。建议显式指定:SecureRandom.getInstance("NativePRNGNonBlocking")。
我曾经遇到一个血泪教训:一个安全日志系统需要生成随机 ID,开发人员图省事用了 java.util.Random 加上时间种子。结果攻击者通过注册几个账号,收集到一批 ID 后就恢复了种子,进而预测出所有管理员的会话 ID。整件事情从发现到被利用,只用了 3 天。事后复盘,如果当初直接用 SecureRandom,只需要改一行代码。

本章知识图谱

密钥生成随机数生态 熵源 硬件 TRNG 用户输入 环境噪声 熵池 混合 / 提取 种子 (seed) CSPRNG CTR_DRBG Hash_DRBG HMAC_DRBG 密钥 不合格PRNG LCG / 梅森旋转 ❌ 永远不要用于密钥 核心结论 熵源 -> 熵池 -> 种子 -> CSPRNG -> 密钥 这条链路中任何一环出错,密钥就不可靠。 普通 PRNG(LCG/梅森)不能用于密码学。 CSPRNG 必须使用经过认证的实现(如 NIST DRBG)。

总结一下:随机数安全不是一个“选个函数调一下”这么简单的事。你需要理解熵的来源、PRNG 的局限、以及 CSPRNG 的保障。我建议你每做一个涉及密钥的项目,都亲手捋一遍这个链路——熵源够不够?种子是哪里来的?CSPRNG 实现是否符合标准?

养成这个习惯之后,你再也不会为“这个随机数安全吗”而纠结了。