电脑怎么产生随机数?伪随机、真随机与密码学安全随机数完整解析

你点下"随机"按钮,程序在瞬间给了你一个数字。这个过程看起来简单,背后却涉及一个计算机科学的根本悖论:电脑是确定性的机器——相同的输入,永远产生相同的输出。那它怎么可能"真正随机"?从幸运转盘到密码生成器,不同工具对"随机"的要求完全不同,而这个差异,在某些场景下会决定你的账户是否安全。

1. 电脑为什么不能"真正随机"?

传统电脑是确定性图灵机的实现:所有操作都依照明确的规则执行,相同的初始状态必然导致相同的后续状态。这意味着任何纯粹在电脑内部运行的算法,其输出都是由初始值(称为"种子",seed)完全决定的。

如果你知道种子,你就能预测所有后续的"随机"数字。这类算法生成的数字序列,在统计上看起来像随机的(通过各种随机性测试),但本质上是可预测的、周期性的伪随机序列。这就是为什么计算机科学领域把它们称为"伪随机数生成器"(Pseudo-Random Number Generator,PRNG)。

"伪"字不代表没用
伪随机并不是坏事——对于模拟、游戏、统计抽样等大多数应用场景,PRNG 完全够用。问题只在于需要"不可预测性"的场景(密码学)——这时伪随机就是一个严重的安全漏洞。

2. 伪随机数生成器(PRNG)的原理

2.1 线性同余生成器(LCG)

最古老也最简单的 PRNG 之一。公式如下:

X(n+1) = (a × X(n) + c) mod m

其中 a(乘数)、c(增量)、m(模数)是预先设定的常数,X(0) 是种子。每次调用就根据前一个值计算出下一个值。

LCG 速度极快、内存占用极少,但质量有限:周期有限,且在多维空间中的分布会呈现规律性的超平面结构,被统计测试容易检测出来。早期的 ANSI C rand() 函数就是 LCG,现在已经很少单独使用。

2.2 梅森旋转算法(Mersenne Twister)

1998 年由松本真和西村拓士提出,MT19937 是目前应用最广泛的 PRNG 之一。它的周期长达 2^19937 − 1,并通过了几乎所有统计随机性测试。Python 的 random 模块、Ruby、PHP 的 rand() 等语言的默认随机函数,都使用梅森旋转。

然而,梅森旋转也有一个严重的弱点:它的内部状态是 624 个 32 位整数。如果攻击者观察到连续 624 个输出值,就能完全重建内部状态,从而预测所有后续输出。这让它完全不适合任何需要安全性的应用。

2.3 现代高质量 PRNG

近年出现了一批更现代的 PRNG,如 xorshift128+、PCG(Permuted Congruential Generator)、xoshiro256**,它们在速度、统计质量和周期上都有优秀表现,但同样不适合密码学应用。

算法周期速度统计质量密码学安全?
LCG(线性同余)中等极快一般
梅森旋转 MT199372^19937−1优良
PCG / xoshiro2^128 以上极快优良
ChaCha20(CSPRNG)不适用优良
硬件随机数(TRNG)不适用真随机

3. 真随机:从物理世界获取不可预测性

要突破确定性电脑的限制,就必须引入来自外部世界的熵(Entropy)——真正不可预测的物理事件。操作系统和硬件通过多种方式收集这些熵:

  • 用户行为:鼠标移动轨迹、键盘按键时机——人类的动作具有极高的不可预测性
  • 硬件中断时机:磁盘读写、网络数据包到达的精确时间点,受到电磁噪声等物理因素的影响
  • CPU 时序抖动:处理器的微小时间差异(jitter),受温度、电压波动等物理因素影响
  • 专用硬件随机数生成器(HRNG/TRNG):利用量子效应(如热噪声、放射性衰变、光子行为)直接产生真随机位的硬件设备

Linux 的 /dev/urandom/dev/random 就是操作系统维护的熵池接口,收集上述各种硬件事件,为密码学应用提供高质量的随机种子。Intel 的 RDRAND 指令则直接在 CPU 层级提供硬件随机数。

/dev/random vs /dev/urandom:有什么区别?
/dev/random 在熵池不足时会"阻塞"等待更多熵积累;/dev/urandom 在熵不足时会继续使用 CSPRNG 延伸输出,不阻塞。现代系统上,初始化后的 /dev/urandom 对几乎所有应用已足够安全。

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

密码学安全随机数生成器(CSPRNG)结合了 PRNG 的速度与熵源的不可预测性。它必须满足两个核心要求:

  • 下一个比特不可预测性:即使知道所有已输出的比特,也无法以优于 50% 的概率预测下一个比特。
  • 状态妥协后向安全性:即使攻击者在某个时刻获取了 CSPRNG 的内部状态,也无法回溯推算出过去已生成的输出。

常见的 CSPRNG 包括 ChaCha20(Linux kernel 4.8+ 的 /dev/urandom 实现基础)、Fortuna(Windows BCryptGenRandom 的算法基础)、以及 NIST 标准化的 Hash_DRBG / HMAC_DRBG。

5. 为什么 Math.random() 绝不能用于密码?

这是最常见也最危险的误解之一。几乎所有语言内置的"基础随机函数"——JavaScript 的 Math.random()、Python 的 random.random()、PHP 的 rand()——都使用 PRNG 而非 CSPRNG。

以 JavaScript 的 Math.random() 为例,研究人员展示:只需观察 3 个连续的 Math.random() 浮点数输出,就可以完全重建内部状态,从而预测所有后续的"随机"数字。

正确的做法是使用专为密码学设计的 API:

  • JavaScript/Web:window.crypto.getRandomValues()
  • Node.js:crypto.randomBytes()
  • Python:secrets 模块(Python 3.6+)
  • PHP:random_bytes()random_int()
  • Java:java.security.SecureRandom
种子固定 = 结果完全可预测
许多人在学习时会写 random.seed(42) 让结果可重现——这在教学和测试中是好的实践。但这也直接说明了 PRNG 的本质:种子一旦固定,所有输出都是确定的。如果你的种子是可猜测的(如当前时间戳),攻击者只需尝试有限的种子值,就能还原你的所有"随机"输出。

6. 不同应用场景的随机数需求

6.1 游戏与娱乐:PRNG 已足够

骰子模拟、幸运转盘、抽签工具、卡牌游戏——这类应用的核心需求是分布均匀、速度快、用户感受到"公平"。梅森旋转或现代 PRNG 完全满足需求。

6.2 统计模拟:PRNG + 种子控制

蒙特卡罗模拟、统计抽样、机器学习的数据分割——需要高质量的统计分布,同时希望结果可重现以便验证。固定种子的 PRNG 是标准做法。

6.3 密码学与安全:必须使用 CSPRNG

密码生成、加密密钥、Token、Session ID、CSRF Token、一次性密码(OTP)——这些场景的安全性完全依赖随机数的不可预测性。任何 PRNG 都是不可接受的,必须使用操作系统提供的 CSPRNG 接口。

应用场景推荐类型示例 API关键需求
游戏/转盘/骰子PRNGMath.random()均匀分布、速度
统计模拟PRNG(固定种子)random.seed(n)可重现性、统计质量
密码/密钥/TokenCSPRNGcrypto.getRandomValues()不可预测性
科学实验TRNGRDRAND / random.org真实熵源

7. 小结

电脑产生随机数的机制,从表面的"点一下就出现数字"背后,隐藏着丰富的技术层次:

  • PRNG(伪随机):确定性算法,速度快、统计质量好,但本质可预测——适合游戏、模拟,不适合密码学
  • TRNG(真随机):从物理世界收集熵,真正不可预测——速度慢,用于提供种子或特殊需求
  • CSPRNG(密码学安全随机数):结合 PRNG 的速度与熵源的不可预测性——密码、密钥、Token 的唯一正确选择

下次你使用密码生成器或看到"随机"功能时,可以问一个问题:这个场景需要的是"看起来随机",还是"真的无法预测"?这个区别,可能就是你数据安全的边界所在。