Farbling
Why it matters Fingerprinting needs no storage at all, so partitioning and storage clearing cannot touch it. One defence is to make every browser report the same values. Another, which you build here, is to add a tiny amount of noise to the most identifying readings, different on every site. Each site still works, but two sites comparing notes see two different devices.
Imagine wearing glasses with slightly different frames in each shop you visit. Staff in one shop recognise you all afternoon, because your frames there do not change. But if two shops compare their descriptions of you, they do not match. Tomorrow you pick new frames everywhere.
Canvas and audio readings: a page can draw a hidden picture or play a silent tone and read back the exact numbers your device produced. Tiny differences between graphics chips and audio stacks make these numbers a strong fingerprint (Leg 1 measured them).
Farbling: the name Brave gave to adding small, per-site noise to such readings. It is a kind of randomisation; the opposite approach is uniformity, making everyone report the same value.
Session key: a secret random value the browser picks each time it starts. Mixing it into the noise means tomorrow's noise is different from today's.
Brave farbles by default, deriving its noise from a per-session secret and the site you are on, and Floati's engine uses the same kind of randomisation (checked October 2026).
What the practice world reads
Pages in the practice world can read two lists of numbers from page.traits: canvas_pixels (24 values) and audio_samples (12 values), each between 0 and 255. A fingerprint script hashes all of Sam's traits, these two lists included, into one ID.
The three rules of good noise
- Different per site. The noise must depend on the top-level site, so ShopA and ShopB get different readings and different fingerprint IDs.
- Stable within a site and session. The same site must get the same noise on every page load, or its own features (such as "is this the device you signed in from?") start failing.
- Small. Each value may move by at most 1. A shop that draws a chart checks its pixels are within 2 of what it expects; more noise than that and the chart looks broken.
Noise you can repeat: a seeded random generator
Python's random module can make a generator from a seed. The same seed always gives the same sequence of "random" numbers, which is exactly rule 2. Build the seed from the session key and the site, and you get rule 1 for free:
import random
rng = random.Random("session-41|shopa.example") # the seed is any text you like
print([rng.choice([-1, 0, 1]) for _ in range(5)]) # e.g. [0, -1, 1, 1, 0]
rng = random.Random("session-41|shopa.example") # same seed again...
print([rng.choice([-1, 0, 1]) for _ in range(5)]) # ...same five numbersrng.choice(list) picks one item. To keep a value inside 0 to 255 after adding noise, clamp it: min(255, max(0, value)). max(0, v) lifts anything below 0 up to 0, and min(255, ...) pulls anything above 255 down.
Do not put the page's full address in the seed. Then every page of the same shop would get different noise, breaking rule 2. And never use Python's own hash() for this: it changes every time Python starts.
How you will know it worked
run_farbling(farble) loads each shop twice in each of two sessions, with your function changing the readings, and reports:
"linked_pairs": pairs of shops the fingerprint tracker could join (want 0)"broken": shops whose chart came out wrong (want none)"unstable": shops that read different values on two loads in one session (want none)"linked_sessions": shops the tracker recognised again in the next session (want none)
An honest limit
Noise only defeats trackers that need an exact match. A determined script could try to average readings or round them away, which is why real engines also cap how precisely some readings can be taken, and make other traits uniform (Leg 2's "Defeat fingerprinting" lab). Farbling is one layer, not the whole wall.
Try it If you have Brave, open a fingerprint test page on two different sites, or in two sessions, and compare the canvas values: they change. In any browser, open DevTools and press Ctrl+Shift+F (Cmd+Option+F on a Mac) to search every script the page loaded for toDataURL or getImageData, two common ways a script reads the canvas back.
In other languages
Seeded noise in JavaScript. JavaScript's Math.random cannot be seeded, so engines use a small generator of their own, seeded from a hash of the session key and site:
function seededRandom(seed) { // mulberry32: tiny, repeatable, fine for noise
return () => {
seed |= 0; seed = (seed + 0x6D2B79F5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function farble(values, seed) {
const rnd = seededRandom(seed);
return values.map(v => Math.min(255, Math.max(0, v + Math.floor(rnd() * 3) - 1)));
}Rust, using the rand crate with a seed:
use rand::{rngs::StdRng, Rng, SeedableRng};
fn farble(values: &[u8], seed: u64) -> Vec<u8> {
let mut rng = StdRng::seed_from_u64(seed);
values.iter().map(|&v| (v as i16 + rng.gen_range(-1..=1)).clamp(0, 255) as u8).collect()
}Recap
- Farbling adds tiny noise to identifying readings, different for every site and every session.
- A seeded random generator makes noise repeatable: seed it with the session key and the site.
- Keep it small and stable, or the sites' own features break.
- It defeats exact-match fingerprinting; real engines layer it with uniform values and limits.
Write farble(values, top_site, session_key). It returns a new list the same length as values, without changing values. Make a generator with random.Random(session_key + "|" + top_site), add rng.choice([-1, 0, 1]) to each value, and clamp each result to 0 to 255. Then set report = run_farbling(farble) and print it.
This lesson is locked
Lessons open one at a time. Finish the previous lesson to unlock this one.