安全随机数生成:强加密的基础
安全随机数生成对加密至关重要。了解crypto.getRandomValues的工作原理以及弱随机性如何破坏安全模型。
安全随机数生成是所有密码学的根基。在 JavaScript 中,crypto.getRandomValues(buffer) 从操作系统的 CSPRNG(Linux/macOS 上的 /dev/urandom,Windows 上的 BCryptGenRandom,iOS/macOS 上的 SecRandomCopyBytes)中读取密码学安全随机字节并填充到 TypedArray 中。凡是涉及安全的场景,一律不要使用 Math.random()——它本质上是 Mulberry32 或 xorshift 风格的伪随机数生成器,设计目标是速度而非不可预测性,只需观察几个输出值就能还原其内部状态。弱随机数生成器会直接破坏 AES 密钥、TLS 握手、GCM 模式的 nonce 唯一性、令牌不可猜测性,以及所有依赖不可预测比特的安全原语。
普通随机与密码学随机的根本区别
PRNG(伪随机数生成器)从种子出发产生确定性数据流。知道种子和算法,就能复现每一个输出。这对游戏、仿真和蒙特卡洛方法完全够用,但对密码学是灾难性的。
CSPRNG(密码学安全伪随机数生成器)从真实熵源(热噪声、中断时序、Intel RDSEED 等硬件随机指令)获取种子,其设计保证输出在计算意义上与真随机数不可区分,观察历史输出无法预测未来输出。
JavaScript 两者都有:Math.random() 是 PRNG,crypto.getRandomValues() 是对操作系统 CSPRNG 的封装。一行代码的差别,安全性天壤之别。
正确的标准用法
// 生成 256 位 AES 密钥所需的随机字节
const keyBytes = crypto.getRandomValues(new Uint8Array(32));
// 生成 96 位 GCM nonce
const nonce = crypto.getRandomValues(new Uint8Array(12));
// 生成 128 位 salt
const salt = crypto.getRandomValues(new Uint8Array(16));
// 生成 URL 安全的随机令牌
const tokenBytes = crypto.getRandomValues(new Uint8Array(32));
const token = btoa(String.fromCharCode(...tokenBytes))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
crypto.getRandomValues() 是同步调用,原地填充缓冲区并返回该缓冲区。单次调用最多请求 65,536 字节(规范规定的配额,防止阻塞)。需要更多随机材料时,多次调用即可。
Node.js 等效方式
const { randomBytes, webcrypto } = require('crypto');
const keyBytes = randomBytes(32); // 返回 Buffer
// 或与 Web Crypto 兼容的写法
const nonce = webcrypto.getRandomValues(new Uint8Array(12));
Node 的 randomBytes 和 Web Crypto 使用同一个底层 CSPRNG。对于同时运行在浏览器和服务端的同构代码,webcrypto.getRandomValues 与浏览器行为完全一致。
为什么 Math.random 不能用于安全场景
V8(Chrome/Node)、SpiderMonkey(Firefox)和 JavaScriptCore(Safari)均将 Math.random() 实现为快速 PRNG,不提供任何密码学保证。V8 使用 xorshift128+ 变体。研究人员已证明,观察约五个输出后,攻击者即可还原内部状态并预测所有未来输出。2015 年,Mike Pound 等人在真实漏洞赏金项目中成功逆向了 V8 的 Math.random 内部状态。
如果用 Math.random() 生成会话令牌、密码重置链接、加密 nonce 或分享 ID,攻击者观察几个样本后就能预测其余所有值。这不是理论问题,而是安全审计中的常见漏洞类型。
常见误用场景
用 Math.random() 作为库 PRNG 的种子:
// 错误做法
const seed = Math.floor(Math.random() * 2**32);
下游的随机性不可能超过种子本身,应改用 crypto.getRandomValues(new Uint32Array(1))[0]。
使用 Date.now() 作为熵源: 时间戳在很短的时间窗口内是可猜测的。即便与少量随机因子结合,时间戳仍会泄露足够多的比特供攻击者利用。
自行 XOR 多个来源: 不要这样做。操作系统的 CSPRNG 已经混合了所有有用的熵源,自行搅拌通常会减少熵而不是增加。
取模偏差: randomBytes[0] % 10 在 0–9 上分布不均,因为 256 不是 10 的倍数。要在范围内生成均匀随机整数,应使用拒绝采样:
function randomInt(max) {
const range = new Uint32Array(1);
const threshold = 2**32 - (2**32 % max);
do {
crypto.getRandomValues(range);
} while (range[0] >= threshold);
return range[0] % max;
}
熵源与启动时风险
在 Linux 上,/dev/urandom 在早期启动完成后始终安全。但在没有硬件 RNG 的系统上,启动后最初几秒内核熵池可能未充分填充。2008 年 Debian OpenSSL 漏洞正是利用了这一点——一个补丁删除了熵混合逻辑,只留下进程 ID 作为种子,导致生成的密钥仅有 2^15 种可能值,几秒内即可穷举。
现代系统从以下来源为内核 CSPRNG 提供种子:x86-64 上的 RDSEED(如有)、ARMv8.5-A RNG 指令、各种外设的热噪声、中断时序,以及交互式系统的键盘和鼠标。在搭载 Intel Ice Lake 或 AMD Zen 3+ 的服务器上,CSPRNG 在启动后数微秒内即可完成种子填充。
Docker 容器默认透传宿主机的 /dev/urandom,无需额外操作。对于无服务器环境(AWS Lambda、Cloudflare Workers),运行时负责为每次调用进行熵填充。
会话令牌与分享 ID
文件传输服务需要生成以下随机标识符:
- URL 中的文件 ID(攻击者不应能猜到有效 ID)
- 密码保护链接的分享令牌
- CSRF 令牌
- 加密密钥(每文件 AES 密钥)
- GCM 的 nonce
最小长度:128 位(16 字节)用于防碰撞和不可猜测性,256 位(32 字节)用于密钥。经 base64url 编码后长度增加约 33%,hex 编码增加 100%。一个 32 字节的 base64url 编码令牌长 43 个字符,在 2^256 的搜索空间下碰撞概率趋近于零。
随机字符串与 UUID
对于面向用户的标识符,crypto.randomUUID() 返回 v4 UUID(122 位随机性),格式标准:
const id = crypto.randomUUID();
// "f47ac10b-58cc-4372-a567-0e02b2c3d479"
支持 Chrome 92+、Firefox 95+、Safari 15.4+、Node 14.17+,适合用作数据库主键、API 请求 ID 及非安全关键的唯一标识符。需要自定义格式或更高熵时,请直接使用 getRandomValues。
检测弱随机数生成器
以下迹象表明 RNG 存在问题:
- 不同请求生成了相同的令牌(在理论上应极其庞大的空间中发生碰撞)
- 输出通过视觉检验但无法通过
dieharder或PractRand统计测试套件 - 进程重启后复用种子——每次部署都使用相同的初始状态
- 生成的密钥呈现规律性(例如前 4 字节变化,后 28 字节完全相同)
代码审计建议:对代码库中的每次 Math.random() 调用都应复查。每周通过 grep Math.random 扫描源码树是基本的安全卫生措施。将任何安全相关调用点改为 crypto.getRandomValues 只需几分钟,却能防范真实漏洞。
服务端:避免自定义随机数池
某些服务框架提供自己的随机数池,声称将系统 CSPRNG 与应用层熵"混合"。对此应保持警惕——自定义混合鲜少能提升内核输出的质量,一旦存在缺陷还可能悄无声息地降低熵。在 Node 或主流运行时上,内置的 crypto.randomBytes 既正确又高效,不要用第三方混合器替换它。
核心原则
文件传输应用中的每一个密码学操作都依赖不可预测的随机字节。AES 密钥、GCM nonce、PBKDF2 salt、分享令牌、防 CSRF 令牌、会话 ID——所有这些都需要同一个原语:浏览器中用 crypto.getRandomValues(),Node 中用 crypto.randomBytes() 或 webcrypto.getRandomValues。就用这些,永远不要用 Math.random(),也不要用时间戳,更不要自行混合。HexaTransfer 的每个文件 AES 密钥、nonce 和 URL 标识符均由客户端的 crypto.getRandomValues() 派生,服务端分享 ID 来自 crypto.randomBytes,一套 API,行为一致,不存在将可预测比特意外引入系统的可能。
在 hexatransfer.com 上试试 — 免费、无需注册、最多 10 GB。