# 浏览器指纹值采样与回放指南 当目标 JS 访问 Canvas、WebGL、WebGPU、Audio、字体、DOM 几何、CSS 渲染等浏览器指纹 API,或 Node.js 中第三方库模拟结果与真实浏览器不一致时读取本文件。 ## 核心原则 补环境的目标是让目标网页原始 JS 在 Node.js 中看到与真实浏览器一致的结果,而不是在 Node.js 中复刻完整浏览器渲染管线。 必须遵循: - **指纹基线先固定**:采样前先读取 `fingerprint-baseline-consistency.md`,确认 `case/notes/fingerprint-baseline.json` 和 `baselineId` 已创建;所有采样必须来自同一 profile / seed / 代理 / 语言 / 时区 / UA / Client Hints / screen / WebGL 基线。 - **Trace 未截断值优先**:如果用户选择 / 提供了 RuyiTrace 或其他 trace 日志,先使用 trace 中未截断、与当前 `baselineId` 一致、能确认完整性的真实浏览器值。 - **Trace 不可用再采样**:未选择 trace、trace 缺失、trace 未覆盖目标路径、trace 字符串 / 序列化值达到或接近 4000 / 4096 字符疑似截断、真实长度为 `unknown`、或 trace baseline 冲突时,必须使用用户当前确认的取证工具采集真实浏览器最终返回值、调用参数、调用栈和调用顺序。 - **禁止 AI 猜指纹值**:AI 经验、静态分析、默认值、随机值、mock 值、Node.js / jsdom / node-canvas / headless-gl 的结果不能作为最终指纹回放值,只能辅助定位和设计采样点。 - **终端 API 值回放优先**:在 Node.js 中拦截目标真正读取指纹结果的 API,并返回采样值。 - **不真实模拟渲染过程**:不要为了 Canvas / WebGL / 字体 / DOM 几何去强行复现 Skia、GPU、字体栅格化、抗锯齿、颜色管理或浏览器布局。 - **不退回自动化作为最终方案**:Node.js 无法真实渲染时,不得建议把最终生成参数或最终验证改成浏览器自动化;自动化只允许用于前置取证和采样。 - **缺样本即显式阻塞**:没有采到的指纹值不要乱猜,不要静默返回空值;应提示补采样本或降级说明。 - **长指纹值不得用截断片段**:Canvas dataURL、WebGL readPixels、WebGPU adapter / limits / features、Audio channel data、字体 / DOM 几何批量结果等可能远超 4000 / 4096;只要来自会截断的日志字段,就必须视为定位证据而不是最终值,最终 fixture 必须来自同一 baseline 下的真实浏览器完整采样。 错误方向: ```text node-canvas / headless-gl / 自己实现 DOM 布局 → 结果与真实浏览器仍不一致 → 建议最终用自动化浏览器 ``` 正确方向: ```text 真实浏览器采集终端 API 返回值 → Node.js 按调用特征匹配并回放 → fixtures 多样本验证 ``` ## 指纹值来源优先级 采集任何具体 WebAPI / 指纹值时,必须按以下顺序判断,不能跳级猜值: 1. **Trace 未截断真实值**:用户已选择 RuyiTrace / 提供 NDJSON,且字段未达到截断阈值、日志覆盖当前业务路径、调用栈与目标参数生成链路相关、`baselineId` 与当前 case 一致。 2. **同一取证工具补采完整值**:Trace 未选择、缺失、未覆盖、疑似截断或 baseline 不一致时,使用用户已确认的取证模式采样,例如 ruyiPage、Camoufox、CloakBrowser、用户手动浏览器或对应 MCP / Hook。采样必须复用同一 profile / seed / 代理 / locale / timezone / UA / Client Hints / screen / WebGL 基线。 3. **用户提供的真实浏览器材料**:用户明确提供的 HAR、Hook 输出、浏览器控制台输出、完整响应或截图可作为证据,但必须记录来源、时间、页面、baseline 信息和完整性。 4. **静态分析 / AI 推断仅辅助定位**:只能用于判断应该采样哪个 API、哪个调用参数、哪个 JS 位置,不能直接写入最终 fixture 的 `result`。 如果 trace 中字符串达到或接近 4000 / 4096 字符,必须标记为“疑似截断”:`truncated: true`、`realLength: "unknown"`、`visibleLength`、`sha256OfVisible`。该值不得作为最终回放值;必须用同一 case 已确认取证工具补采完整值。 ### 长指纹值截断硬门禁 以下规则是硬性要求: 1. RuyiTrace / NDJSON / 普通日志中的长字段达到或接近 4000 / 4096 时,只能说明“至少有这些可见字符”,不能说明真实长度就是 4000 / 4096。 2. 不能把 `visiblePreview`、`visibleSha256`、`sha256OfVisible`、日志可见片段、`...`、`...`、`__ruyiTraceLongString__` 对象或被日志 sanitization 包装后的字段写成最终 `result`。 3. Canvas `toDataURL`、Canvas `getImageData`、WebGL `readPixels`、WebGPU `adapter.info / limits / features`、Audio `getChannelData / startRendering`、字体和 DOM 几何批量结果,如果 trace 中达到 4000 / 4096,必须由已确认取证工具重新采样完整浏览器真实值。 4. 完整采样值可以直接存储,也可以使用分片格式存储;分片格式必须记录 `encoding: "string-chunks"`、`valueLength`、`chunkSize`、`chunks`、`truncated: false` 和完整值 hash。 5. 没有完整真实值时,阻塞最终交付;不要把 4096 字符当成“刚好完整”,也不要用 Node.js 渲染库补一个近似值。 6. 数值型返回值为 4096 不一定是截断,例如 WebGL `MAX_TEXTURE_SIZE`;本规则针对日志中的长字符串、长数组、base64、序列化对象和二进制编码结果。 每个最终写入 fixture 的样本都要能回答: - 值来自哪里:RuyiTrace 未截断值 / ruyiPage 采样 / Camoufox 采样 / CloakBrowser 采样 / 用户手动浏览器材料 / 其他真实浏览器证据。 - 是否完整:`truncated: false`,并记录完整 `valueLength` 与 `sha256`;疑似截断值不得用于最终回放。 - 属于哪个基线:`baselineId` 必须与 `case/notes/fingerprint-baseline.json` 一致。 - 为什么不用 Trace:如果没有使用 trace,写明未选择、缺失、未覆盖、截断、baseline 冲突或用户明确降级。 ## 过程 API 与终端 API ### 过程 API 过程 API 只描述绘制、布局或状态构造过程。补环境时可记录必要状态,但通常不需要真实执行渲染。 常见过程 API: - Canvas 2D:`fillText`、`strokeText`、`drawImage`、`arc`、`rect`、`fillRect`、`putImageData`。 - WebGL:`createShader`、`shaderSource`、`compileShader`、`attachShader`、`linkProgram`、`bufferData`。 - DOM:`appendChild`、`insertBefore`、`removeChild`、`style.xxx = ...`、`className = ...`。 - 字体:设置 `ctx.font`、创建 span/div、设置 CSS。 ### 终端 API 终端 API 是目标 JS 真正读取指纹结果的位置。这里必须优先回放真实浏览器采样值。 | 类别 | 终端 API | |---|---| | Canvas | `HTMLCanvasElement.prototype.toDataURL`、`toBlob`、`CanvasRenderingContext2D.prototype.getImageData`、`measureText` | | WebGL | `getParameter`、`getSupportedExtensions`、`getExtension`、`getShaderPrecisionFormat`、`readPixels`、`getContextAttributes` | | WebGPU | `navigator.gpu.requestAdapter`、`adapter.info`、`adapter.limits`、`adapter.features`、`requestDevice` | | Audio | `OfflineAudioContext.startRendering`、`AudioBuffer.getChannelData`、`AnalyserNode.getFloatFrequencyData` | | 字体 | `measureText`、`document.fonts.check`、DOM 字体探测元素的宽高 | | DOM 几何 | `getBoundingClientRect`、`getClientRects`、`offsetWidth`、`offsetHeight`、`scrollWidth`、`scrollHeight`、`clientWidth`、`clientHeight` | | CSS / 媒体 | `getComputedStyle`、`matchMedia`、`CSS.supports` | | Speech | `speechSynthesis.getVoices`、`SpeechSynthesisUtterance` 属性、voices 列表顺序与语言 | | Permissions | `navigator.permissions.query` 的 Promise、`PermissionStatus.state`、事件属性、错误模式 | | Plugins / MimeTypes | `navigator.plugins`、`navigator.mimeTypes`、`PluginArray` / `MimeTypeArray` 长度、索引、命名属性和描述符 | | MediaDevices / WebRTC | `navigator.mediaDevices.enumerateDevices`、`getUserMedia` 错误模式、`RTCPeerConnection` 候选信息摘要 | ## 高强度指纹补充与随机化风险 高强度检测中,Canvas / WebGL / Audio / Fonts / DOM geometry / Permissions / Plugins 等结果被随机化或伪装不一致,可能比固定真实值更容易暴露。补环境时必须遵循: - 不要使用 `Math.random()`、时间戳、机器默认值或 AI 经验生成指纹返回值。 - 不要把 `node-canvas`、`headless-gl`、`jsdom`、系统字体枚举结果直接当成最终浏览器样本。 - 不要每次取证重新生成一套 UA、语言、时区、screen、WebGL、Canvas、Audio 或字体结果;同一 case 只能使用同一 `baselineId`。 - 对 `navigator.plugins` / `navigator.mimeTypes`,addon 可用时优先 `getMimeTypesAndPlugins(config)`;不要用普通数组或空数组临时糊弄。 - 对 Permissions / Speech / MediaDevices 等异步 API,要采样 Promise 行为、错误类型、事件属性、返回对象原型链和 `toStringTag`,不能只补一个同步值。 - 如果 trace 中长字段接近 4000 / 4096 字符,按截断处理并补采完整值,不能把可见片段作为最终 fixture;完整值必须来自同一 baseline 下的真实浏览器采样。 ## 采样格式 推荐把指纹样本放入 `case/fixtures/fingerprint.fixture.json`,或合并进业务 fixture 的 `fingerprint` 字段。fixture 必须包含 `baselineId`,并与 `case/notes/fingerprint-baseline.json` 一致。 ```json { "version": 1, "baselineId": "fp-20260627-001", "source": { "baselineId": "fp-20260627-001", "mode": "ruyiPage + RuyiTrace / Camoufox / CloakBrowser / 手动取证", "valuePriority": "trace-untruncated-first", "traceStatus": "unused / used-untruncated / missing / not-covered / truncated / baseline-conflict", "traceFallbackReason": "trace 字段疑似截断,已使用 ruyiPage 在同一 baseline 下补采完整值", "pageUrl": "https://example.com/page", "userAgent": "", "timezone": "Asia/Shanghai", "locale": "zh-CN", "capturedAt": "" }, "canvas": { "toDataURL": [ { "match": { "width": 300, "height": 150, "type": "image/png", "stackIncludes": "fingerprint.js" }, "source": { "capturedBy": "ruyiPage", "traceStatus": "truncated", "baselineId": "fp-20260627-001", "truncated": false, "valueLength": 123456, "sha256": "..." }, "result": { "encoding": "string-chunks", "valueLength": 123456, "chunkSize": 3000, "chunks": ["data:image/png;base64,..."], "truncated": false, "sha256": "..." } } ], "measureText": [ { "match": { "text": "abcdef", "font": "16px Arial" }, "result": { "width": 123.45, "actualBoundingBoxLeft": 0, "actualBoundingBoxRight": 123.45 } } ], "getImageData": [ { "match": { "sx": 0, "sy": 0, "sw": 16, "sh": 16 }, "result": { "width": 16, "height": 16, "dataBase64": "..." } } ] }, "webgl": { "getParameter": [ { "match": { "pname": 37445 }, "result": "Google Inc. (NVIDIA)" }, { "match": { "pname": 37446 }, "result": "ANGLE (...)" } ], "getSupportedExtensions": { "result": ["ANGLE_instanced_arrays", "WEBGL_debug_renderer_info"] }, "readPixels": [ { "match": { "x": 0, "y": 0, "width": 16, "height": 16 }, "result": { "dataBase64": "..." } } ] }, "domGeometry": { "getBoundingClientRect": [ { "match": { "selector": "#fp" }, "result": { "x": 0, "y": 0, "width": 100, "height": 20, "top": 0, "left": 0, "right": 100, "bottom": 20 } } ], "offset": [ { "match": { "selector": "#font-probe" }, "result": { "offsetWidth": 123, "offsetHeight": 19 } } ] } } ``` 注意: - `baselineId` 是硬性字段;缺失或与 baseline 文件不一致时,不能把该 fixture 用于最终交付。 - `source` / `capturedBy` / `traceStatus` / `truncated` 是硬性溯源字段;不能只写“AI 推断”“默认值”“静态分析得到”。 - 如果样本继承全局 `source`,短值可以接受;但长字符串、二进制、Canvas / WebGL / WebGPU / Audio 结果必须写入每条记录自己的 `source`、`valueLength`、`sha256` 和 `truncated: false`,必要时使用 `encoding: "string-chunks"` 分片保存。 - `match` 是调用特征,不是安全边界;只用于选择回放样本。 - `stackIncludes` 可记录目标 JS 文件名、函数名或调用栈片段。 - 对 ArrayBuffer / Uint8ClampedArray / Float32Array 等二进制结果使用 base64 存储。 - 真实 Cookie、账号标识、Authorization 不要写入指纹 fixture。 ## 回放匹配顺序 Node.js 交付环境中匹配指纹样本时,按以下顺序: 1. API 名称。 2. 显式参数,例如 `type`、`quality`、`pname`、`text`、`font`、`x/y/w/h`。 3. 对象状态,例如 canvas `width/height`、当前 `ctx.font`、WebGL context 类型。 4. 调用栈特征,例如 `stackIncludes`。 5. 调用顺序,仅作为最后兜底。 如果无法唯一匹配: - 优先报错并输出缺失的 call key。 - 不要返回随机值。 - 不要为了通过而返回空字符串、空数组或固定默认值。 ## 禁止与允许 禁止: - 最终项目引入 `playwright`、`puppeteer`、`selenium`、`cloakbrowser`、`ruyipage` 来计算指纹。 - 最终项目用自动化打开页面生成指纹,再把结果传回 Node.js。 - 因 `node-canvas` / `headless-gl` / `jsdom` 输出不一致而建议改用自动化作为最终方案。 - 未采样时静默伪造 Canvas / WebGL / DOM 几何结果。 允许: - 前置取证阶段用用户确认的工具采集指纹 API 参数、返回值和调用栈。 - Node.js 交付环境中用 `assets/env-modules/fingerprint-env.js` 模板回放采样值。 - 对不影响目标参数的 API 做明确降级,但必须在 notes 中说明证据等级。 - 对目标未访问的 API 不补。 ## 真实性保护要求 即使终端 API 只是返回采样值,也要保持浏览器对象真实性: - 用 `Object.defineProperty` 定义属性。 - 建立 `HTMLCanvasElement`、`CanvasRenderingContext2D`、`WebGLRenderingContext`、`DOMRect` 等必要原型链。 - 保护方法、getter、setter 的 `Function.prototype.toString`。 - 设置实例对象 `Symbol.toStringTag` / `Object.prototype.toString`。 - 按浏览器行为抛出 `TypeError` 或 `Illegal invocation`,不要暴露 Node.js 错误。 ## 推荐执行流程 1. 先读取 `fingerprint-baseline-consistency.md`,确认或创建 `case/notes/fingerprint-baseline.json` 与 `baselineId`。 2. 用 RuyiTrace NDJSON 或 Hook 找到目标 JS 是否访问指纹 API;如果用户选择 / 提供 RuyiTrace,先判断 trace 中是否已有未截断真实值可用。 3. 如果访问的是 Canvas / WebGL / WebGPU / Audio / 字体 / DOM 几何,且 trace 没有未截断完整值,或 trace 字段达到 / 接近 4000 / 4096,再生成采样 Hook;采样 Hook 必须完整保存长值或分片保存,不得裁剪为 4096: ```bash node scripts/generate_fingerprint_hook.js --types canvas,webgl,dom-geometry --out case/hooks/fingerprint-hook.js ``` 4. 在用户确认的取证工具中运行采样 Hook,触发最少量业务动作;复制结果前必须执行 `await window.__WEB_JS_ENV_PATCHER_FINALIZE_FINGERPRINT__()`,确保所有分片长值写入完整 `valueLength`、`sha256`、`chunks` 与 `truncated:false`。 5. 把 finalize 后的采样结果保存为 `case/fixtures/fingerprint.fixture.json`;禁止保存 `clip` 片段、`visiblePreview`、`sha256OfVisible`、`__ruyiTraceLongString__` 或任何真实长度 unknown 的 trace 可见片段。 6. 运行覆盖检查: ```bash node scripts/check_fingerprint_fixture.js --fixture case/fixtures/fingerprint.fixture.json --require canvas,webgl --markdown ``` 7. 把 `assets/env-modules/fingerprint-env.js` 复制到最终项目 `result/src/env/`,按 fixture 接入。 8. 对目标 JS 运行 fixtures,若缺样本,补采样而不是改用自动化。 9. 交付前运行: ```bash node scripts/check_env_realism.js --case-dir case --require-fingerprint-fixture --markdown node scripts/check_final_artifact.js --case-dir case --markdown ``` ## 输出记录要求 补环境报告或 notes 中至少记录: - `baselineId`、baseline 文件路径和是否发生 baseline diff。 - 哪些指纹 API 被目标 JS 访问。 - 哪些值来自 RuyiTrace 未截断日志,哪些值因为 trace 缺失 / 未覆盖 / 截断 / baseline 冲突而改用自动化取证工具采样。 - 哪些值来自 ruyiPage / Camoufox / CloakBrowser / Hook / 用户手动浏览器材料,并记录采样工具、时间、完整长度和 hash。 - 是否存在疑似截断字段;如果存在,必须写明补采状态,不能把可见长度当成真实长度,也不能把 trace 中 4000 / 4096 字符片段当成最终指纹值。 - 哪些 API 做了近似降级及原因。 - 缺失样本是否阻塞。 - 最终项目是否完全不包含浏览器自动化代码。