跳转到内容
HexaTransfer
返回博客
加密与安全

安全随机数生成:强加密的基础

安全随机数生成对加密至关重要。了解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 存在问题:

  • 不同请求生成了相同的令牌(在理论上应极其庞大的空间中发生碰撞)
  • 输出通过视觉检验但无法通过 dieharderPractRand 统计测试套件
  • 进程重启后复用种子——每次部署都使用相同的初始状态
  • 生成的密钥呈现规律性(例如前 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。

通过端到端加密安全发送大文件

通过端到端加密免费传输最大10GB的文件。无需注册账户。文件在上传前在浏览器中加密,其他人无法读取。

发送文件