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
canvasHashis:
canvasHash = SHA1(geometryDataURL)
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;
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(),
};
}
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);
Stability decision:
const text1 = canvas.toDataURL();
const text2 = canvas.toDataURL();
if (text1 !== text2) {
canvasHash = "unstable";
}
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);
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");
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
}
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
}
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
canvasHashis a 40-character SHA1 hex string. - The source stable branch is
SHA1(geometry).toString(). - The runtime writer output in
CANVAS_DETAIL.canvasHashis 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:
- Read the page's native displayed 40-character
canvasHash. - Inject
canvas_fp_pure.jsinto the current page. - Run the restored
collectCanvasFingerprint(document)with the page's realdocument/canvas. - 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"
}
}
This proves, in the same browser page environment:
- The page's own
canvasHashexactly matchescanvas_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
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);
Stable output:
{
stable: true,
text: firstTextDataURL,
geometry: geometryDataURL,
hash: SHA1(geometryDataURL)
}
Unstable output:
{
stable: false,
text: "unstable",
geometry: "unstable",
hash: "unstable"
}
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>
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";
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.jsperforms the stability decision, SHA1, and helper parsing
That is the fully restored pure-computation part of the browserscan.net Canvas fingerprint.
Top comments (0)