DEV Community

LoseNine
LoseNine

Posted on

Browserscan.net Canvas Fingerprint Pure-JS Technical Report

browserscan.net Canvas Fingerprint Pure-JS Technical Report

Join my Discord community to learn, share, and discuss together: https://discord.gg/rX2vkNNW

Quick Conclusion

The /canvas page on browserscan.net splits the Canvas fingerprint into two stages:

  • It first draws a text canvas and calls canvas.toDataURL() twice to check output stability.
  • If the two text dataURLs differ, the result is "unstable".
  • If they are stable, it draws a geometry canvas and reads geometryDataURL.
  • The final displayed and reported canvasHash is:
canvasHash = SHA1(geometryDataURL)
Enter fullscreen mode Exit fullscreen mode

Important: the final hash input is the complete PNG dataURL of the geometry canvas, not the text canvas.

This directory contains canvas_fp_pure.js, which reproduces:

  • the Canvas drawing instructions
  • the stability decision
  • pure-JS SHA1
  • PNG dataURL length and chunk/CRC helper parsing

Logic Chain

Layer Script / Function Purpose
Page entry yfrOX3ej.js -> ta() Triggers Canvas detection and reports CANVAS_DETAIL
Hash builder yfrOX3ej.js -> ea() Calls the core collector and computes SHA1(geometry) when stable
Core collector Dfh5dMcW.js -> T0() Produces { winding, text, geometry }
Text drawing Dfh5dMcW.js -> X(canvas, ctx) Produces the text image used for stability checking
Geometry drawing Dfh5dMcW.js -> x0(canvas, ctx) Produces the image used as the final hash input
dataURL wrapper Dfh5dMcW.js -> B(canvas) Wraps canvas.toDataURL()

The page-level logic reduces to:

const { text, geometry } = await T0();

const isStable = text !== "unstable";
const canvasHash = isStable ? SHA1(geometry).toString() : geometry;
Enter fullscreen mode Exit fullscreen mode

The core collector reduces to:

async function T0() {
  const [canvas, ctx] = createCanvas2d();

  if (!supportCanvasPngDataUrl(canvas, ctx)) {
    return {
      winding: false,
      text: "unsupported",
      geometry: "unsupported",
    };
  }

  const winding = checkEvenOddWinding(ctx);

  await drawText(canvas, ctx);
  const text1 = canvas.toDataURL();
  const text2 = canvas.toDataURL();

  if (text1 !== text2) {
    return {
      winding,
      text: "unstable",
      geometry: "unstable",
    };
  }

  await nextTick();
  drawGeometry(canvas, ctx);

  return {
    winding,
    text: text1,
    geometry: canvas.toDataURL(),
  };
}
Enter fullscreen mode Exit fullscreen mode

Text Canvas

The text canvas is only used for stability checking. It is not directly hashed.

canvas.width = 240;
canvas.height = 60;

ctx.textBaseline = "alphabetic";

ctx.fillStyle = "#f60";
ctx.fillRect(100, 1, 62, 20);

ctx.fillStyle = "#069";
ctx.font = '11pt "Times New Roman"';
ctx.fillText("Cwm fjordbank gly 😃", 2, 15);

await nextTick();

ctx.fillStyle = "rgba(102, 204, 0, 0.2)";
ctx.font = "18pt Arial";
ctx.fillText("Cwm fjordbank gly 😃", 4, 45);
Enter fullscreen mode Exit fullscreen mode

Stability decision:

const text1 = canvas.toDataURL();
const text2 = canvas.toDataURL();

if (text1 !== text2) {
  canvasHash = "unstable";
}
Enter fullscreen mode Exit fullscreen mode

Geometry Canvas

After the text canvas is stable, the same canvas is resized to 122 x 110 and redrawn. This geometry dataURL is the SHA1 input.

canvas.width = 122;
canvas.height = 110;

ctx.globalCompositeOperation = "multiply";

ctx.fillStyle = "#f2f";
ctx.beginPath();
ctx.arc(40, 40, 40, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();

ctx.fillStyle = "#2ff";
ctx.beginPath();
ctx.arc(80, 40, 40, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();

ctx.fillStyle = "#ff2";
ctx.beginPath();
ctx.arc(60, 80, 40, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();

ctx.fillStyle = "#f9c";
ctx.arc(60, 60, 60, 0, Math.PI * 2, true);
ctx.arc(60, 60, 20, 0, Math.PI * 2, true);
ctx.fill("evenodd");

const geometryDataURL = canvas.toDataURL();
const canvasHash = SHA1(geometryDataURL);
Enter fullscreen mode Exit fullscreen mode

Winding Check

winding is an auxiliary result. It is not part of the final hash input.

ctx.rect(0, 0, 10, 10);
ctx.rect(2, 2, 6, 6);

const winding = !ctx.isPointInPath(5, 5, "evenodd");
Enter fullscreen mode Exit fullscreen mode

Environment and Anti-Detection Fields

The page also computes capability and anti-detection fields. These are displayed and reported, but they do not change the main hash formula when the canvas is stable.

Function Meaning
R0() Canvas 2D context support
u0() Canvas text API support
I0() Whether toDataURL() outputs a PNG dataURL
S0() OffscreenCanvas support
l0() Whether toDataURL.toString() looks native
r0() Pixel comparison after reloading the canvas dataURL into an Image
e0() DOM canvas vs OffscreenCanvas output comparison
c0()/Q()/q() Canvas/WebGL noise and WebGL parameter checks
U0() Aggregates isNotAntiBrowser

The reported object looks like:

{
  type: "CANVAS_DETAIL",
  canvasHash,
  supportContext2d,
  supportTillText,
  supportDataUrl,
  supportOffscreenCanvas,
  canvasNative,
  isStable,
  isNotAntiBrowser
}
Enter fullscreen mode Exit fullscreen mode

Trace Evidence Summary

The dropped domtrace/jscall/http evidence shows the script relationship:

Script Role
Dfh5dMcW.js Core Canvas/WebGL fingerprint logic
yfrOX3ej.js /canvas page component and hash caller
CCZS-DkR.js Shared dependency providing SHA1

Key DOM calls observed in the main flow:

Call Purpose
document.createElement("canvas") Creates the detection canvas
canvas.getContext("2d") Gets the 2D context
ctx.fillText(...) Draws the text image
ctx.fillRect(...) Draws the text image background block
ctx.arc(...) / ctx.fill(...) Draws the geometry image
canvas.toDataURL() Outputs PNG dataURL
ctx.getImageData(...) Pixel comparison for anti-detection checks
OffscreenCanvas.getContext("2d") OffscreenCanvas comparison

Note: DOM trace string logging truncates long strings at 4000 characters. Long toDataURL values in the trace can confirm call order and prefixes, but they must not be treated as complete PNG dataURLs or final hash samples. The final hash must be computed from the runtime-complete geometryDataURL.

Direct Runtime Verification

A fresh run of https://www.browserscan.net/canvas captured this runtime CANVAS_DETAIL:

{
  "type": "CANVAS_DETAIL",
  "canvasHash": "ab684eb07ee596f8ed673c7e4eaaaa3f061195d0",
  "supportContext2d": true,
  "supportTillText": true,
  "supportDataUrl": true,
  "supportOffscreenCanvas": true,
  "canvasNative": true,
  "isStable": true,
  "isNotAntiBrowser": true
}
Enter fullscreen mode Exit fullscreen mode

In the same run, the T0() toDataURL() sequence was:

Order Role Logged length Notes
1 PNG dataURL support check 154 P(canvas, ctx)
2 T0() support check 154 Confirms PNG output before main collection
3 First text sample 4000 Trace string truncated
4 Second text sample 4000 Truncated prefix matches the first one; runtime isStable=true
5 Geometry sample 4000 Trace string truncated; the runtime-complete value is passed into SHA1

Verification conclusion:

  • The runtime page took the stable branch: isStable=true.
  • The final canvasHash is a 40-character SHA1 hex string.
  • The source stable branch is SHA1(geometry).toString().
  • The runtime writer output in CANVAS_DETAIL.canvasHash is consistent with that branch.
  • The exact hash may change across Canvas rendering environments and PNG encoder details; the stable part is the formula and flow, not a fixed hash constant.

ruyiPage Same-Context Strong Verification

Using the C:\ruyipage automation framework, the verification was run inside the same browserscan page context:

  1. Read the page's native displayed 40-character canvasHash.
  2. Inject canvas_fp_pure.js into the current page.
  3. Run the restored collectCanvasFingerprint(document) with the page's real document/canvas.
  4. Compare the page-native hash with the restored local hash.

Result:

{
  "ok": true,
  "pageHashBeforeInject": "979686afccc6b098f6ccc9b0122c4ea56ce1bd13",
  "verify": {
    "pageHash": "979686afccc6b098f6ccc9b0122c4ea56ce1bd13",
    "localHash": "979686afccc6b098f6ccc9b0122c4ea56ce1bd13",
    "matches": true,
    "stable": true,
    "winding": true,
    "textLength": 7806,
    "geometryLength": 8750,
    "geometryBytes": 6544,
    "geometryPngCrcOk": true,
    "geometryChunkTypes": ["IHDR", "IDAT", "deBG", "IEND"],
    "readyState": "complete",
    "location": "https://www.browserscan.net/canvas"
  }
}
Enter fullscreen mode Exit fullscreen mode

This proves, in the same browser page environment:

  • The page's own canvasHash exactly matches canvas_fp_pure.js.
  • The text canvas is stable.
  • The geometry PNG is complete and passes CRC validation.
  • The restored drawing flow and SHA1 logic match the actual browserscan execution.

Validation script: ruyipage_verify_browserscan.py

Validation output: ruyipage_verify_result.json

canvas_fp_pure.js Usage

Self-test:

node --check canvas_fp_pure.js
node canvas_fp_pure.js --self-test
Enter fullscreen mode Exit fullscreen mode

Pure hash computation from existing dataURLs:

const fp = require("./canvas_fp_pure");

const result = fp.computeCanvasHashFromDataUrls({
  text1: firstTextDataURL,
  text2: secondTextDataURL,
  geometry: geometryDataURL,
});

console.log(result);
Enter fullscreen mode Exit fullscreen mode

Stable output:

{
  stable: true,
  text: firstTextDataURL,
  geometry: geometryDataURL,
  hash: SHA1(geometryDataURL)
}
Enter fullscreen mode Exit fullscreen mode

Unstable output:

{
  stable: false,
  text: "unstable",
  geometry: "unstable",
  hash: "unstable"
}
Enter fullscreen mode Exit fullscreen mode

Run the full collection inside a real browser Canvas environment:

<script src="./canvas_fp_pure.js"></script>
<script>
  BrowserScanCanvasFP.collectCanvasFingerprint(document).then(console.log);
</script>
Enter fullscreen mode Exit fullscreen mode

Main API:

API Purpose
sha1(input) Pure-JS SHA1
computeCanvasHashFromDataUrls({ text1, text2, geometry }) Reproduces the stability decision and final hash
buildTextDrawOps() Returns the restored text drawing instructions
buildGeometryDrawOps() Returns the restored geometry drawing instructions
drawTextFingerprint(canvas, ctx) Draws the text image on a real Canvas
drawGeometryFingerprint(canvas, ctx) Draws the geometry image on a real Canvas
collectCanvasFingerprint(document) Runs the full collection in a browser
dataUrlByteLength(dataURL) Computes decoded dataURL byte length
parsePngChunks(dataURL) Parses PNG chunks and validates CRC32

Pure-Computation Boundary

The browserscan algorithm itself can be reproduced as pure logic:

stable = textDataURL1 === textDataURL2;
canvasHash = stable ? SHA1(geometryDataURL) : "unstable";
Enter fullscreen mode Exit fullscreen mode

However, geometryDataURL is not generated by a pure hash algorithm. It is a Canvas rendering artifact affected by:

  • fonts
  • anti-aliasing
  • graphics stack
  • color blending
  • PNG encoder behavior
  • Canvas noise or anti-fingerprinting policy

The practical engineering boundary is:

  • a real Canvas implementation, or an equivalent renderer, produces text1/text2/geometry
  • canvas_fp_pure.js performs the stability decision, SHA1, and helper parsing

That is the fully restored pure-computation part of the browserscan.net Canvas fingerprint.

Top comments (0)