2. 密钥生成基础:熵源、随机数与密钥算法
大家好,我是你们这门课的老朋友。今天咱们聊点硬核的——密钥到底是怎么变出来的?
很多人觉得生成密钥不就是调用一个函数吗?KeyGenerator.getInstance("AES") 就完事了。你要是这么想,那这课就白上了(笑)。密钥生成是整个密码系统的根基,根基不牢,地动山摇。我亲眼见过因为随机数出问题,整个业务系统被打穿的情况。
2.1 熵源:密钥的原材料
先问个问题:计算机是确定性的机器,它怎么产生“随机”?
答案是——靠“熵”。熵是物理世界的不确定性。你按键盘的时间间隔、鼠标的移动轨迹、磁盘的读写延迟,这些都是熵。没有熵,就没有真正的随机。
核心概念:
- 熵源(Entropy Source):系统从物理环境中采集的不确定性数据。
- 熵池(Entropy Pool):操作系统把采集到的熵混合在一起,形成一个“随机数原料池”。
- 熵估计算法:判断采集到的数据到底有多少“真随机性”的算法。
我遇到过最典型的坑是什么?是嵌入式设备。有些IoT设备刚上电,熵池几乎是空的。你这时候去调用 /dev/random,它就卡在那等熵。我一个项目里,设备启动后要生成TLS证书,结果卡了整整两分钟——因为熵不够。
避坑指南:
我曾经遇到过生产环境某台虚拟机长期运行后,熵池耗尽导致 SSL 握手失败。排查了三天才发现是熵源不足。从那以后,我所有的服务器都会装 haveged 或者 rng-tools,专门补充熵源。
常见的熵源对比:
| 熵源类型 | 质量 | 采集速度 | 典型场景 |
|---|---|---|---|
| 硬件随机数生成器(HRNG) | ★★★★★ | 极快 | 服务器、HSM、安全芯片 |
| 操作系统的熵池(/dev/random) | ★★★★ | 慢(可能阻塞) | 通用软件生成密钥 |
| /dev/urandom(Linux) | ★★★ | 快(非阻塞) | 一般应用密码学(除一次一密外) |
| 用户交互源(鼠标、键盘) | ★★★ | 极慢 | PGP密钥生成,个人场景 |
| 伪随机发生器(无熵注入) | ★ | 极快 | 严禁用于任何密钥生成场景 |
2.2 随机数生成器:从熵到种子
有了熵,下一步就是把它变成可用的随机数。这里有两个概念必须分清:真随机数生成器(TRNG)和伪随机数生成器(PRNG)。
TRNG 直接从物理熵源输出随机数。它不可预测,但速度慢,产量低。
PRNG 用有限的熵去“扩展”出大量随机数。你想想看,每次生成AES-256密钥需要256位高质量随机数,如果每次都去采集熵,那效率太低了。所以实际做法是:用TRNG采集一小段高质量随机数作为种子(Seed),然后交给PRNG去“复制粘贴”——当然不是真的复制粘贴,是用密码学安全的算法把种子拉伸成任意长度的伪随机序列。
关键要求:
- PRNG必须使用密码学安全伪随机数生成器(CSPRNG)。千万别用
rand()、random()或Math.random()。 - CSPRNG的特性:即使攻击者拿到了之前生成的随机数,也无法推算出后续的随机数(前向安全)。
我自己在写代码时,有一个铁律:
- Java:只用
SecureRandom,并且指定SHA1PRNG或NativePRNG算法。 - Python:只用
secrets模块或os.urandom。 - C/C++:调用操作系统的
getrandom()或BCryptGenRandom(Windows)。 - Go:只用
crypto/rand。
// Java 示例:正确使用 SecureRandom
import java.security.SecureRandom;
public class KeyGeneratorExample {
public static byte[] generateKey(int keySizeBytes) {
SecureRandom secureRandom = new SecureRandom();
// 重点:调用 nextBytes 时会自动从操作系统熵池获取种子
byte[] key = new byte[keySizeBytes];
secureRandom.nextBytes(key);
return key;
}
}
工程实践小贴士:
我建议所有涉及密钥生成的程序,在启动时主动触发一次 SecureRandom.getInstanceStrong() 来初始化熵池。这样能避免运行时首次调用因熵不足而阻塞。
2.3 对称密钥生成算法
有了好的随机数,对称密钥生成就简单了——本质就是产生足够长的、均匀分布的随机比特串。
举个例子:生成一个AES-256密钥,就是生成256位(32字节)的真随机数据。没有比这更复杂的了。
注意:
我曾经在检查代码时,发现有人用 UUID.randomUUID() 来生成AES密钥。UUID只有128位,而且包含固定格式的版本位和变体位,实际可用随机空间只有122位。更可怕的是,UUID的随机性不是密码学安全的。千万别这么干!
常见的对称密钥长度和算法:
- AES:128位(12字节)、192位(24字节)、256位(32字节)。我个人习惯直接上256位,多花不了多少性能,安全性翻倍。
- SM4:国密标准,128位密钥。
- ChaCha20:256位密钥,在移动端性能极好。
2.4 非对称密钥生成算法
非对称密钥生成就复杂多了。因为它不是简单的随机比特串,而是要生成一对符合特定数学结构的密钥。
以RSA为例,生成过程需要:
- 随机生成两个大质数 p 和 q。
- 计算 n = p × q。
- 计算 φ(n) = (p-1) × (q-1)。
- 选择一个公钥指数 e(常用 65537)。
- 计算私钥指数 d,满足 e × d ≡ 1 mod φ(n)。
这里面每一步都需要高质量的随机数。尤其第1步,生成大质数的时候,需要随机选数,再通过Miller-Rabin素性测试去验证。你想想看,生成一个2048位的RSA密钥,背后要做多少次随机数调用?
而ECC(椭圆曲线密码)稍微不同,它的私钥本质上就是一个随机大整数,公钥是私钥乘以基点。所以ECC密钥生成比RSA快很多,密钥也短。我对ECC的态度一直是:能用ECC就别用RSA,除非有历史兼容性要求。
| 算法 | 私钥长度 | 公钥长度 | 生成速度 | 安全强度(对标AES) |
|---|---|---|---|---|
| RSA-2048 | 2048位 | 2048位 | 慢(需找大质数) | ~ 112位 |
| RSA-4096 | 4096位 | 4096位 | 很慢 | ~ 128位 |
| ECC P-256 | 256位 | 256位(压缩) | 快 | ~ 128位 |
| ECC P-384 | 384位 | 384位(压缩) | 较慢 | ~ 192位 |
| SM2(国密) | 256位 | 256位(压缩) | 快 | ~ 128位 |
经验之谈:
我在做金融系统时,密钥量特别大(几十万对),每次生成RSA-4096要几秒。后来换成ECC P-384,生成时间降到毫秒级。说白了,选算法不光看安全级别,还得看工程场景。
2.5 工程实践中的关键决策
讲完了原理,咱们聊聊实际该怎么选。我总结了几个原则:
- 能用硬件别用软件:如果你的设备有硬件随机数生成器(比如 Intel 的 RdRand 指令、安全芯片),优先使用。硬件熵源的信噪比远高于软件采集。
- 生产环境必须监控熵池水位:Linux 下可以通过
/proc/sys/kernel/random/entropy_avail查看。如果这个值长期低于 100,你的系统处于危险状态。我曾经用过阿里云的某款低配ECS,默认竟然没有安装 rngd,熵池长期只有 16。这样的环境你敢用来生成密钥? - 密钥生成后的“熵清洁”:生成密钥后,把密钥生成过程中使用的临时变量(特别是大质数 p、q)立刻擦除。在 Java 中,用
Arrays.fill()把数组清零;在 C 中用memset_s()。别让它们在内存里待着,否则一旦被 dump 出 core 文件,密钥就全暴露了。 - 测试环境也得上强度:我见过不少开发在测试环境用
12345678当密钥。等上生产了,忘记改成真随机数。流水线上直接被秒破。所以测试环境也一律用真正的 CSPRNG 生成密钥,只是密钥生命周期短一点就行。
一句话总结本章:
密钥生成 = (物理熵源 → 操作系统熵池 → CSPRNG种子扩展) + (对称密钥 = 随机比特串 / 非对称密钥 = 随机数 + 数学推导)。缺一不可,顺序不能乱。
好了,这一章的内容就到这。记住:密钥生成是整个密码系统里唯一不能“抄近道”的环节。在这个阶段偷的懒,后面用十倍的精力也补不回来。