随机数:加密系统中最被低估的组件
1. 为什么要关心这个问题?
2010 年,PlayStation 3 的安全系统被完全破解。不是因为加密算法有问题,而是因为 Sony 在数字签名时用了一个固定的「随机」数。
# Sony 的致命错误(简化版)
def sign_game(game_data):
k = 4 # 这个数字应该每次都不同!
signature = ecdsa_sign(game_data, private_key, k)
return signature结果?任何人都可以计算出 Sony 的私钥,为任意代码签名,在 PS3 上运行盗版游戏和自制软件。
一个「随机数」不随机,整个安全系统崩溃。
这不是个案。从 Debian OpenSSL 漏洞(2008)到 Android Bitcoin 钱包被盗(2013),历史上无数安全灾难都源于随机数问题。
2. 定义
随机数在密码学中指的是不可预测的数值,用于生成密钥、初始化向量(IV)、nonce、盐值等。
密码学安全的随机数必须具备:
- 不可预测性: 即使知道之前生成的所有数字,也无法预测下一个
- 不可重现性: 相同的条件不会产生相同的序列
- 均匀分布: 所有可能的值出现概率相等
关键区别:
- 真随机数(TRNG): 来自物理现象,真正不可预测
- 伪随机数(PRNG): 算法生成,看起来随机但实际上是确定性的
- 密码学安全伪随机数(CSPRNG): 特殊设计的 PRNG,即使知道部分输出也无法预测其他输出
3. 为什么随机数这么重要
密钥生成
密钥 = random(256 bits)
如果 random() 可预测:
- 攻击者可以猜测密钥
- 加密形同虚设一个 256 位的 AES 密钥有 2^256 种可能。但如果你的随机数生成器只能生成 2^32 种不同的密钥(因为种子只有 32 位),攻击者只需要尝试 42 亿次——几小时就能完成。
初始化向量(IV)
AES-CBC 加密:
ciphertext = AES_CBC(plaintext, key, IV)
如果 IV 可预测:
- 攻击者可以进行选择明文攻击
- 即使密钥安全,加密仍可能被破解数字签名中的 Nonce
这就是 Sony PS3 被破解的原因:
ECDSA 签名:
signature = sign(message, private_key, k)
如果 k 重复使用:
private_key = (message1 - message2) / (signature1 - signature2)
↑ 直接计算出私钥!4. rand() 为什么会害死人
标准函数库的 rand() 不是为安全设计的
// C 语言的 rand() - 绝对不要用于密码学!
int seed = time(NULL); // 种子是当前时间
srand(seed);
int key = rand(); // 「随机」密钥问题:
- 种子太小: 通常只有 32 位,最多 42 亿种可能
- 种子可预测:
time(NULL)是当前秒数,攻击者大概知道你何时生成密钥 - 算法简单: 线性同余生成器(LCG),数学上容易逆推
# Python 的 random 模块 - 同样不安全!
import random
random.seed() # 使用系统时间
key = random.getrandbits(256) # 不要这样做!真实案例:Debian OpenSSL 灾难(2008)
// 有人「修复」了这段代码中的警告
MD_Update(&m, buf, j); // 被删除
^
|
Valgrind 说这个变量未初始化
MD_Update(&m, &(md_c[0]), sizeof(md_c)); // 保留这个「修复」移除了熵的主要来源。结果:
- OpenSSL 只能生成 32,768 种不同的密钥
- 所有在受影响系统上生成的 SSH 和 SSL 证书都可以被暴力破解
- 影响了数百万台 Debian/Ubuntu 服务器
5. 真随机 vs 伪随机
真随机数(TRNG)
来源:物理现象
- 放射性衰变
- 热噪声
- 量子效应
- 鼠标移动、键盘时序
物理现象 → 测量 → 数字化 → 真随机比特优点:真正不可预测 缺点:慢、需要特殊硬件、比特率有限
伪随机数(PRNG)
种子 → 算法 → 看起来随机的序列# 简化的 PRNG 算法
def prng(seed):
state = seed
while True:
state = (state * 1103515245 + 12345) % 2**31
yield state优点:快、可重现(对测试有用) 缺点:完全确定性——知道算法和种子就知道全部输出
密码学安全伪随机数(CSPRNG)
专门为安全设计的 PRNG:
熵池 ──► CSPRNG ──► 安全的随机输出
↑
持续收集熵特性:
- 即使看到部分输出,也无法预测其他输出
- 即使内部状态被泄漏,也无法恢复之前的输出
- 持续从环境收集熵来更新内部状态
6. 操作系统的随机性来源
Linux: /dev/random vs /dev/urandom
# 阻塞式 - 熵不足时会等待
head -c 32 /dev/random
# 非阻塞式 - 总是立即返回
head -c 32 /dev/urandom现代建议:几乎总是使用 /dev/urandom
历史上的担忧(已过时):
/dev/random「更安全」因为会等待真熵/dev/urandom可能「熵不足」
现实:
- 现代 Linux 的 urandom 使用相同的熵池
- 系统启动后,urandom 在密码学上与 random 一样安全
/dev/random的阻塞只会造成问题(DoS、性能)
熵的来源
┌─────────────────────────────────────────────────────────────┐
│ 硬件熵来源 │
├─────────────────────────────────────────────────────────────┤
│ - CPU 时序抖动 │
│ - 硬件随机数生成器(RDRAND/RDSEED 指令) │
│ - TPM(可信平台模块) │
│ - 专用熵生成硬件 │
├─────────────────────────────────────────────────────────────┤
│ 软件熵来源 │
├─────────────────────────────────────────────────────────────┤
│ - 中断时序 │
│ - 磁盘 I/O 时序 │
│ - 网络数据包时序 │
│ - 键盘/鼠标事件 │
└─────────────────────────────────────────────────────────────┘Windows: CryptGenRandom / BCryptGenRandom
// Windows API
BYTE buffer[32];
BCryptGenRandom(NULL, buffer, 32, BCRYPT_USE_SYSTEM_PREFERRED_RNG);macOS/iOS: SecRandomCopyBytes
var bytes = [UInt8](repeating: 0, count: 32)
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)7. 各语言的正确用法
Python
import secrets # Python 3.6+,专为密码学设计
# 生成安全的随机字节
key = secrets.token_bytes(32) # 256 位密钥
# 生成安全的十六进制字符串
token = secrets.token_hex(32) # 64 字符的 hex 字符串
# 生成 URL 安全的 token
url_token = secrets.token_urlsafe(32)
# 在范围内选择安全的随机数
dice = secrets.randbelow(6) + 1 # 1-6
# 安全的随机选择
password_char = secrets.choice('abcdefghijklmnopqrstuvwxyz0123456789')# 绝对不要这样做!
import random
key = random.getrandbits(256) # 不安全!Node.js
const crypto = require('crypto');
// 生成安全的随机字节
const key = crypto.randomBytes(32);
// 生成安全的 UUID
const { randomUUID } = require('crypto');
const uuid = randomUUID();
// 生成范围内的安全随机数
function secureRandomInt(min, max) {
const range = max - min;
const bytesNeeded = Math.ceil(Math.log2(range) / 8);
const randomBytes = crypto.randomBytes(bytesNeeded);
const randomValue = parseInt(randomBytes.toString('hex'), 16);
return min + (randomValue % range);
}// 绝对不要这样做!
const key = Math.random() * 2**256; // 不安全!Go
import (
"crypto/rand"
"encoding/hex"
)
// 生成安全的随机字节
func generateKey() ([]byte, error) {
key := make([]byte, 32)
_, err := rand.Read(key)
if err != nil {
return nil, err
}
return key, nil
}
// 生成安全的十六进制字符串
func generateToken() (string, error) {
bytes := make([]byte, 32)
_, err := rand.Read(bytes)
if err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}// 绝对不要这样做!
import "math/rand"
key := rand.Int63() // 不安全!Rust
use rand::Rng;
use rand::rngs::OsRng;
fn main() {
// 使用 OS 的 CSPRNG
let mut key = [0u8; 32];
OsRng.fill(&mut key);
// 生成范围内的安全随机数
let n: u32 = OsRng.gen_range(1..=100);
}8. 常见错误与防范
错误 1:使用时间作为种子
# 错误
import random
random.seed(int(time.time()))
key = random.getrandbits(256)
# 攻击者知道你大概何时生成密钥
# 只需要尝试那段时间内的所有秒数错误 2:重复使用 nonce/IV
# 错误
iv = b'0' * 16 # 固定 IV
for message in messages:
ciphertext = aes_cbc_encrypt(message, key, iv) # 相同 IV!错误 3:缩小随机数范围
# 错误
import secrets
key = secrets.randbelow(1000000) # 只有 100 万种可能!
# 正确
key = secrets.token_bytes(32) # 2^256 种可能错误 4:在虚拟机快照后不重新播种
VM 快照 → 恢复 → 生成「随机」数
↑
和上次快照后生成的一样!某些 CSPRNG 在 VM 恢复后需要重新收集熵。
错误 5:自己实现随机数生成器
# 错误:「我设计了一个更好的算法」
def my_random():
global state
state = (state * 31337 + 12345) ^ (time.time_ns() % 256)
return state
# 这几乎肯定会有安全问题9. 测试随机数品质
基本统计测试
import secrets
from collections import Counter
# 生成大量随机数
samples = [secrets.randbelow(256) for _ in range(100000)]
# 检查分布
counter = Counter(samples)
for i in range(256):
expected = 100000 / 256
actual = counter[i]
if abs(actual - expected) / expected > 0.1: # 超过 10% 偏差
print(f"警告:值 {i} 的分布异常")NIST 随机数测试套件
# 使用 NIST SP 800-22 测试套件
# https://csrc.nist.gov/projects/random-bit-generation/documentation-and-software
./assess 1000000 # 测试 100 万比特测试项目包括:
- 频率测试(0 和 1 的比例)
- 块频率测试
- 游程测试
- 最长游程测试
- 矩阵秩测试
- 离散傅立叶变换测试
- 等等…
10. 本章小结
三点要记住:
永远使用 CSPRNG。 在任何涉及安全的场景——密钥、IV、nonce、盐值、token——都必须使用密码学安全的随机数生成器。Python 用
secrets,Node.js 用crypto.randomBytes,绝对不要用random或Math.random()。随机数的品质决定加密的强度。 一个 256 位的密钥只有在每一位都是真正随机的情况下才有 2^256 的安全性。如果生成器有缺陷,实际安全性可能只有 2^32 或更低。
重复使用是致命的。 在 ECDSA 中重复 nonce 会直接泄漏私钥。在 AES-CTR 中重复 nonce 会让攻击者用 XOR 恢复明文。每个随机数都必须是一次性的。
11. 下一步
我们已经完成了密码学基础的五个核心概念:加密不等于安全、密码学解决的问题、对称与非对称加密、哈希函数、以及随机数。
在下一部分,我们将深入对称加密的工程实践:从 DES 说起——分组加密的基本思想,为什么要分组,以及 DES 为什么会失败。
