Canvas Fingerprinting, End to End
Demo post. This is a placeholder written to exercise the publishing pipeline. The code below is illustrative — it is not a measurement, a benchmark, or a research finding.
Canvas fingerprinting is one of the oldest and most frequently cited browser signals. The premise is simple: ask the browser to rasterize text and shapes, then read the pixels back out. Differences in GPU, driver, font stack, and anti-aliasing settings mean two machines rarely produce byte-identical output.
Drawing something deterministic
The important word is deterministic. The drawing routine must be identical every time, or the signal becomes noise. Fixed dimensions, fixed colors, fixed text, no timestamps.
function drawProbe() {
const canvas = document.createElement("canvas");
canvas.width = 300;
canvas.height = 60;
const ctx = canvas.getContext("2d");
if (!ctx) return null;
ctx.textBaseline = "top";
ctx.font = '14px "Arial"';
ctx.fillStyle = "#f60";
ctx.fillRect(0, 0, 120, 24);
ctx.fillStyle = "#069";
ctx.fillText("Canvas probe — 🔒", 4, 28);
ctx.globalCompositeOperation = "multiply";
ctx.beginPath();
ctx.arc(180, 30, 22, 0, Math.PI * 2, true);
ctx.fillStyle = "rgba(0, 200, 180, 0.7)";
ctx.fill();
return canvas.toDataURL();
}A couple of notes on the snippet above. getContext("2d") can legitimately
return null — some privacy configurations disable the context entirely, and
that refusal is itself worth recording. Compositing operations such as
multiply are included because blend math tends to differ more across
implementations than flat fills do.
Hashing the output
The data URL is long and unwieldy. What you actually want to carry around is a
short, stable digest. crypto.subtle gives you SHA-256 without a dependency,
though note that it is only available in secure contexts.
async function sha256Hex(input: string): Promise<string> {
const bytes = new TextEncoder().encode(input);
const digest = await crypto.subtle.digest("SHA-256", bytes);
return Array.from(new Uint8Array(digest))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
export async function canvasFingerprint(): Promise<string> {
const dataUrl = drawProbe();
if (dataUrl === null) return "unavailable";
return sha256Hex(dataUrl);
}Why a cryptographic hash?
There is nothing secret here, so collision resistance is not a security requirement — it is a bookkeeping one. A short, fixed-width value is easier to store, index, and compare than a multi-kilobyte base64 blob, and SHA-256 is already in every browser.
Things worth keeping in mind
| Consideration | Why it matters |
|---|---|
| Determinism | Any variable input turns the signal into noise |
| Null contexts | A refusal to render is itself a data point |
| Randomization | Some browsers perturb pixels per-origin or per-session |
| Consent | Collecting device signals has legal and ethical weight |
Several browsers now add deliberate per-origin noise to toDataURL, which means
a single reading can no longer be assumed stable across sessions. Treat any
canvas value as one weak input among many rather than an identifier on its own.