Files

259 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 浏览器指纹值采样与回放指南
当目标 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`、日志可见片段、`...<clipped>`、`...<truncated>`、`__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 做了近似降级及原因。
- 缺失样本是否阻塞。
- 最终项目是否完全不包含浏览器自动化代码。