2. 熵源与种子:高质量随机数的生成、种子熵的计算与安全存储

密钥派生树的根,说白了就是一颗种子。这颗种子要是质量不行,整棵树都是空中楼阁。我见过不少项目,算法选得花里胡哨,结果种子生成环节随便用了个 rand()——嗯,那基本等于没锁门。

这一章我们就把种子这件事彻底讲透。从熵的物理来源,到如何量化它,再到怎么安全地存下来。你想想看,如果连根都烂了,树上结的果子谁敢吃?

2.1 熵到底是什么?

熵,在密码学里不是热力学概念。它衡量的是「不确定性」。一个比特的熵,意味着你猜对它的概率是 1/2。8 比特的熵,就是 1/256。

说白了,熵越高,攻击者需要尝试的次数就越多。我个人习惯把熵理解为「攻击者的工作量」——你每增加 1 比特熵,攻击者的工作量就翻一倍。

核心公式:

种子熵(比特) = log₂(可能状态数)

例如:一个 256 比特的均匀随机数,熵就是 256 比特。

但这里有个坑:均匀随机这四个字是关键。如果你的随机数生成器有偏差,实际熵会远低于理论值。我在项目中遇到过一台硬件随机数发生器,厂商标称 128 比特熵,结果我们实测发现因为电源噪声耦合,低 4 位几乎固定——实际熵不到 100 比特。

2.2 高质量随机数的来源

随机数生成器分两类:真随机数发生器(TRNG)和伪随机数发生器(PRNG)。TRNG 从物理过程提取熵,PRNG 从种子扩展出随机序列。

类型 熵来源 输出速率 典型场景
TRNG 热噪声、量子效应、放射性衰变 慢(kbps ~ Mbps) 种子生成、密钥初始化
PRNG 种子 + 算法扩展 快(Gbps) 会话密钥、随机数批量需求

实际工程中,我们通常混合使用:TRNG 负责产生高熵种子,PRNG 负责把种子扩展成任意长度的随机序列。这个组合叫「混合随机数生成器」。

我的经验:永远不要只用 PRNG 生成种子。PRNG 的种子本身需要 TRNG 提供。如果你用 PRNG 的输出去喂另一个 PRNG,那只是把确定性序列复制了一份,熵并没有增加。

2.3 常见熵源及其质量评估

不同的熵源,质量天差地别。我列几个常见的,你感受一下:

  • 鼠标/触控板移动:熵率低,约 0.5~2 比特/事件。容易被侧信道攻击。
  • 键盘敲击间隔:熵率中等,约 1~4 比特/击键。但用户行为可预测。
  • 麦克风环境噪声:熵率较高,但受环境影响大。安静房间 vs 嘈杂街道,差异巨大。
  • 专用硬件 TRNG:熵率稳定,通常经过认证。比如 Intel 的 RDRAND 指令。
  • 量子随机数发生器:理论上完美熵源。但成本高,体积大。

我曾经帮一个物联网项目做安全审计,他们用 ADC 采样的悬空引脚噪声做熵源。听起来挺合理对吧?结果发现每次上电后前 100 毫秒的采样值几乎一样——因为电容还没放电完。嗯,这个坑我踩过,后来我们加了一个「熵源健康检测」模块,实时评估输出质量。

2.4 种子熵的计算方法

计算种子熵,不能只看种子长度。你得考虑熵源的实际分布。公式很简单:

H = -Σ p(x) · log₂ p(x)

其中 p(x) 是每个可能值的概率。如果所有值等概率,H = log₂(N)。但现实中,熵源往往有偏差。

举个例子:你用一个 8 位 ADC 采集热噪声,理想情况下每样本有 8 比特熵。但如果噪声幅度只覆盖了 4 位的有效范围,实际熵可能只有 4 比特。你采集 256 个样本,理论上有 2048 比特,实际可能只有 1024 比特。

警告:不要用「种子长度」代替「种子熵」。一个 256 位的种子,如果是从偏差源生成的,实际熵可能只有 128 位。攻击者只需要搜索 2¹²⁸ 空间,而不是 2²⁵⁶。

我习惯的做法是:先对熵源做大量采样,用统计工具估算实际熵率,然后乘以采样数量,得到总熵。最后留 20% 的余量——比如你需要 128 比特熵,就采集到 160 比特等效熵的量。

2.5 种子的安全存储

种子生成之后,怎么存?存哪里?这是另一个大坑。

先看几个错误示范:

  • 把种子明文写在配置文件里
  • 用环境变量存种子
  • 种子随日志一起输出
  • 种子被 swap 到磁盘上

正确的做法,我总结为「三不原则」:

  1. 不落盘:种子只在内存中存在,用完立即擦除。
  2. 不共享:每个设备、每个应用使用独立的种子。
  3. 不暴露:种子所在内存页加锁,防止被 swap 到磁盘。

具体实现上,我推荐使用操作系统的安全内存机制:

// Linux 下使用 mlock 防止内存被 swap
#include <sys/mman.h>
#include <string.h>

void secure_store_seed(uint8_t *seed, size_t len) {
    // 锁定内存页
    if (mlock(seed, len) != 0) {
        // 处理错误:无法锁定内存
        return;
    }
    
    // 使用种子...
    
    // 使用完后安全擦除
    volatile uint8_t *p = seed;
    for (size_t i = 0; i < len; i++) {
        p[i] = 0;
    }
    
    // 解锁内存页
    munlock(seed, len);
}

注意那个 volatile 关键字。没有它,编译器可能优化掉擦除操作——我见过不止一次。你以为擦除了,实际上种子还在内存里躺着。

小技巧:在嵌入式设备上,如果内存有限,可以考虑把种子存储在 MCU 的内部 SRAM 中,并启用读保护(RDP)。这样即使攻击者拿到芯片,也无法直接读出种子。

2.6 种子生命周期管理

种子不是生成一次用一辈子。它有自己的生命周期:

阶段 操作 注意事项
生成 从 TRNG 采集足够熵 验证熵源健康状态
存储 加密后写入安全存储 使用设备绑定密钥加密
使用 解密到锁定内存 用完立即擦除
更新 定期或事件触发重新生成 旧种子安全销毁
销毁 多次覆写 + 验证 对 NAND/SSD 需特殊处理

我曾经参与过一个金融级密钥管理系统,种子的更新策略是:每 24 小时强制更新一次,或者检测到异常事件(如温度突变、电压波动)时立即更新。这个策略后来被多个项目借鉴。

2.7 本章知识体系

下面这张图总结了种子从熵源到安全存储的完整链路:

种子生成与安全存储流程 种子生成与安全存储流程 物理熵源 (TRNG) 熵评估与健康检测 种子生成 (≥256位) 安全存储 (加密) 派生密钥树 关键原则:不落盘、不共享、不暴露 每次使用后安全擦除,定期更新种子

从这张图你可以看到,种子不是孤立存在的。它前面连着熵源和评估,后面连着存储和派生。任何一个环节出问题,整条链都不可信。