密钥生成与随机数安全:熵源、PRNG、安全随机数生成器
密钥的本质是什么?说白了,就是一段别人猜不出来的数据。而猜不出来的核心前提——随机性。
你想想看,如果密钥是用一个可预测的方法生成的,那攻击者只要知道你的算法,就能复现你的密钥。我在项目中遇到过不止一次,有人直接用 rand() 来生成 AES 密钥。结果呢?上线不到一周,密钥就被逆推了。
所以,这一章我们聊清楚随机数背后的三个核心概念:熵源、伪随机数生成器(PRNG)、安全随机数生成器(CSPRNG)。搞懂它们,你写出的密钥才不会被轻易“偷走”。
随机数为什么重要?
我习惯把随机数比作“密码世界的氧气”。没有它,加密、签名、密钥交换全都瘫痪。
- 密钥生成:不能用固定数据,必须随机。
- 初始化向量(IV):每次加密必须不同,否则明文模式会泄露。
- 盐值(Salt):密码哈希时需要不可预测的盐。
- nonce / 挑战值:协议里用来防重放。
我见过不少项目,业务逻辑里用 Math.random() 生成登录 。嗯,攻击者只要拿到几个 ,就能用线性同余算法直接预测下一个 的值。这就是没区分“普通随机”和“安全随机”的代价。
熵源:真正的随机从哪里来?
计算机本质上是确定性的机器。它自己无法“创造”真正的随机。所以需要从物理世界中引入不可预测性,这种不可预测性就叫熵。
物理熵源
- 硬件随机数生成器(HRNG):利用热噪声、光电效应、量子效应。比如 Intel 的
RDRAND指令。 - 专用安全芯片:TPM、HSM 内置的 TRNG。
我在做硬件安全模块时遇到过一个问题:某国产芯片的硬件随机数输出竟然有规律可循。拿示波器一看,发现它内部用的是计数器加固定线性变换,根本不是真正随机。后来只能外挂一个独立熵源芯片。
软件熵源
- 用户输入:键盘敲击间隔、鼠标移动轨迹。
- 系统环境:磁盘 I/O 时序、网络包到达时间、硬件中断间隔。
- /dev/random 和 /dev/urandom:Linux 内核从设备驱动收集熵,存在熵池里。
/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);
secrets 模块底层调用了 os.urandom,而 os.urandom 在 Linux 上最终从 /dev/urandom 读数。Windows 则调用了 CryptGenRandom。这些都是经过验证的 CSPRNG。
为什么不用 random 模块?因为它的核心是梅森旋转算法,你没有看错——Python 的 random 在 3.0 之后虽然默认种子来源是 os.urandom,但算法本身不是密码学安全的。一旦攻击者拿到若干输出,就能预测后续。我个人习惯:凡是和密钥、、密码有关的,一律只用 secrets 或 os.urandom。
实战指南:避坑经验
- 不要用
rand()、random()生成密钥。 我曾经审计过一个金融 App,它的签名密钥用 C 的rand()生成,种子是当前时间戳。攻击者只要知道 App 编译时间,就能还原全部密钥。 - 注意熵源耗尽。 在嵌入式设备或 IoT 上,系统启动之初熵池是空的。如果这时候调用
/dev/random,进程会阻塞直到收集够熵。我的解决方案:预置一个硬件 TRNG,或者把开机阶段的随机数请求延迟几秒。 - 虚拟机部署要额外小心。 云上虚拟机共享宿主机,熵源通常不足。可以在 Dockerfile 里安装
rng-tools,或者用haveged作为后备熵源。 - 检查你的 CSPRNG 是否可预测。 有些开发板上的 Java 标准库
SecureRandom默认实现可能是SHA1PRNG,它在某些平台初始化不够随机。建议显式指定:SecureRandom.getInstance("NativePRNGNonBlocking")。
java.util.Random 加上时间种子。结果攻击者通过注册几个账号,收集到一批 ID 后就恢复了种子,进而预测出所有管理员的会话 ID。整件事情从发现到被利用,只用了 3 天。事后复盘,如果当初直接用 SecureRandom,只需要改一行代码。
本章知识图谱
总结一下:随机数安全不是一个“选个函数调一下”这么简单的事。你需要理解熵的来源、PRNG 的局限、以及 CSPRNG 的保障。我建议你每做一个涉及密钥的项目,都亲手捋一遍这个链路——熵源够不够?种子是哪里来的?CSPRNG 实现是否符合标准?
养成这个习惯之后,你再也不会为“这个随机数安全吗”而纠结了。