密码学随机性可观测性与浏览器指纹
用可复现 fixture 对比 Web Crypto getRandomValues 与 crypto-js 随机数实现,并限定指纹观察的隐私边界。
BotBrowser Team
BotBrowser 可以重放一个授权的同源 fixture,记录 API 可用性和有限的输出元数据,但不能证明熵质量、识别个人,也不能把随机值变成稳定的浏览器指纹。可用的比较是:浏览器负责的 crypto.getRandomValues() 合同,与向库级生成器提供随机字节的 crypto-js 公开源码路径。
BotBrowser 可以在授权的同源夹具中重放随机性检查,比较有限的能力和长度字段,并在声明的 profile 或依赖版本变化后指出第一处差异。它不能证明熵质量、从随机样本推断个人,也不能保证库的备用路径适合密码学材料。
TL;DR
crypto.getRandomValues()是填充类型化数组的浏览器 API;它不会暴露可复用的浏览器身份。crypto-js是应用代码。它的随机数路径取决于版本、构建、runtime 全局对象,以及实际加载的源码分支。- 比较能力、源码版本、数组形状和粗粒度输出元数据。不要公开随机值、seed、账户数据或原始熵样本。
- 重复 fixture 可以发现实现变化或备用路径失效,但单次输出不能证明熵质量或指纹唯一性。
Contents
区分两个随机源
Web Cryptography 规范 将 Crypto.getRandomValues() 定义为浏览器对整数类型化数组执行的操作。MDN 参考 记录了支持的数组类型、同步返回和配额边界。底层来源由浏览器持有,错误的视图可能被拒绝;页面代码不会选择 seed。
crypto-js 的所有权模型不同。它的公开源码树包含供库算法使用的随机 word helper、runtime 能力检查和在不同版本间变化的 respaldo。读取实际进入 bundle 的准确版本源码,然后记录它选择了浏览器 crypto 对象、其他 runtime 提供者还是 respaldo。仅凭包名不能判断分支。
这里比较的是可观察合同,而不是复现内部熵。浏览器 fixture 可以记录 crypto.getRandomValues 是否存在、类型化数组是否填充,以及加载的 crypto-js 构建是否返回预期形状。它不能把任一结果变成稳定的设备标识。相关信号检查可参阅浏览器指纹验证和跨 realm 的浏览器 API 完整性。
把源码链接与 fixture 放在一起:crypto-js 随机源码、crypto-js releases 和 BotBrowser 自动化一致性。对应 docs 页面覆盖一致性检查,但没有覆盖这组源码级比较或脱敏规则。
使用可复现 fixture
在声明的新 profile 中运行同一个 HTTPS 页面。下面的 fixture 只记录分支名称、长度和生成字节的短 digest;不会保存随机值本身。
async function observeRandomness() {
const record = {
fixtureId: 'crypto-randomness-01',
browserCrypto: typeof globalThis.crypto?.getRandomValues === 'function',
cryptoJsLoaded: typeof globalThis.CryptoJS !== 'undefined',
browserLength: null,
libraryLength: null,
digestPrefix: null,
outcome: 'pending',
};
try {
if (record.browserCrypto) {
const bytes = new Uint8Array(32);
globalThis.crypto.getRandomValues(bytes);
record.browserLength = bytes.byteLength;
record.digestPrefix = Array.from(bytes.slice(0, 4), value => value.toString(16).padStart(2, '0')).join('');
}
if (record.cryptoJsLoaded && typeof CryptoJS.lib.WordArray.random === 'function') {
const words = CryptoJS.lib.WordArray.random(16);
record.libraryLength = words.sigBytes;
}
record.outcome = record.browserCrypto || record.cryptoJsLoaded ? 'observed' : 'unsupported';
} catch (error) {
record.outcome = error.name === 'QuotaExceededError' ? 'quota-error' : 'api-error';
}
return record;
}
fixture 可复现,是因为输入固定:fixtureId、32 字节浏览器数组、16 字节库请求和相同的已加载包版本。输出故意不是确定性的。可交接的 JSON 示例:
{
"fixtureId": "crypto-randomness-01",
"browserCrypto": true,
"cryptoJsLoaded": true,
"browserLength": 32,
"libraryLength": 16,
"digestPrefix": "redacted",
"outcome": "observed"
}
改变一个变量后重复 fixture:浏览器构建、profile、包版本、bundler 或 runtime。私有运行记录保留 browserVersion、os、profile、cryptoJsVersion 和 testedOn。不要比较随机字节;比较第一个变化的能力或形状字段。
| 行 | 单一变化 | 主要字段 | 负责人 |
|---|---|---|---|
| A | baseline | fixture 的全部字段 | 测试负责人 |
| B | 浏览器构建 | browserCrypto、browserLength、outcome | 浏览器/上下文 |
| C | profile 生命周期 | 能力和结果 | profile |
| D | crypto-js 版本 | cryptoJsLoaded、libraryLength、outcome | 依赖负责人 |
| E | bundler 输出 | 加载和方法路径 | 构建负责人 |
排障与边界
让每次 Web Crypto 请求都不超过字节数限制
Web Crypto 规范要求:类型化数组的 byteLength 超过 65,536 时,getRandomValues() 必须抛出 QuotaExceededError。应检查字节数,而不是元素个数:Uint32Array(16_384) 恰好是 65,536 字节,再多一个元素就会超限。下面的函数只接受 Uint8Array,因此子视图中的元素数也等于字节数;更宽元素的视图应先转换为字节视图:
function fillRandomBytes(output) {
if (!(output instanceof Uint8Array)) {
throw new TypeError('output must be a Uint8Array');
}
const maxBytesPerCall = 65_536;
for (let offset = 0; offset < output.byteLength; offset += maxBytesPerCall) {
const length = Math.min(maxBytesPerCall, output.byteLength - offset);
globalThis.crypto.getRandomValues(output.subarray(offset, offset + length));
}
return output;
}
这只处理 API 的单次调用大小限制;它不会让更大的缓冲区变成确定性结果,不会验证熵质量,也不会让随机字节适合公开。保留 fixture 的错误分类:请求过大属于请求形状错误,不代表随机源较弱,也不是浏览器身份的证据。
如果 browserCrypto 为 false,先检查 origin、realm 和上下文,再改变 profile。如果只有库字段变化,检查解析后的包和加载顺序。quota-error 说明请求大小被拒绝,不代表熵较弱。不要把 digest 前缀当作指纹,只保留有限字段。BotBrowser 可以重放授权 fixture 并比较第一处变化,但不能证明熵质量或个人身份。
不过度解读观察结果
| 观察结果 | 可以支持的判断 | 不能证明的事项 |
|---|---|---|
browserCrypto: true | 页面可以调用浏览器 API | 熵质量、硬件来源或个人身份 |
libraryLength: 16 | 已加载库返回了请求形状 | 哪个提供者提供了字节 |
quota-error | 浏览器拒绝了请求的数组大小 | profile 损坏或随机性较弱 |
cryptoJsVersion 分支变化 | 值得检查的源码或依赖变化 | 浏览器指纹或安全结论 |
如果浏览器字段从 true 变为 false,先检查上下文和页面隔离,再归因于 profile。如果只有库字段变化,核对最终 bundle 的模块入口和初始化顺序。如果 digest 前缀重复,把它视为需要重跑的脱敏 fixture 产物,而不是稳定身份的证据。Web Crypto 建议 和 crypto-js 源码回答不同问题,应保持为独立引用。
保持指纹与随机性分离
指纹是相对稳定或受策略控制的观察集合,例如 user-agent 数据、屏幕指标或功能可用性。随机输出则是有意变化的。把随机样本与 profile 关联可能产生应用标识,但这个标识来自应用数据模型,而不是 getRandomValues()。
对于最小化遥测,保留 fixture id、API 分支、请求长度、包版本、粗粒度结果和时间戳。删除随机值、seed、完整 digest、账户标识和跨站关联。BotBrowser 可以在授权的同源夹具中重放随机性检查并比较 available、fallback、reused 或 error 等有限状态;它不能认证第三方熵测试、从样本推断个人,也不能保证库的备用路径适合密码学用途。
下一篇阅读
参阅确定性浏览器行为和隐私保护验证方法,分别理解可重复性与数据最小化。
| 观察项 | 用途 | 建议保留 | 建议删除 |
|---|---|---|---|
available 或 error | 兼容性支持 | fixture id 和粗粒度状态 | 原始字节和堆栈 |
| 请求数组长度 | 回归排查 | 长度和浏览器主版本 | 完整样本值 |
| seeded fixture digest | 重复性检查 | fixture 修订和仅用于测试的 digest | 生产账户关联 |
| token 重用次数 | 生命周期问题 | 次数和过期区间 | token 文本和 endpoint URL |
| 备用分支 | 产品决策 | 分支名和应用修订 | 页面使用的用户数据 |
实践结论
将浏览器的 getRandomValues() 与 bundle 实际加载的 crypto-js 精确公开源码进行比较。使用固定 fixture,只保留能力和形状元数据,每次重跑只改变一个变量。结果可以解释源码或 runtime 的变化,但不能把随机性变成指纹,也不能证明熵质量。
Public sources
- Web Cryptography API (W3C)
- Crypto.getRandomValues() (MDN)
- crypto-js core.js source
- crypto-js releases
- BotBrowser automation consistency
BotBrowser 支持在授权的同源夹具中重放随机性检查,并比较 available、fallback、reused 或 error 等有限状态。BotBrowser 不能证明熵质量,也不能从随机输出推断个人。
本文属于指南: 什么是浏览器指纹?原理、风险、信号与隐私边界