The fingerprint API is Rust-only: it has no Python SeleniumBase counterpart, and
nothing in parity/api.toml changes. So this is a breaking change, listed in the
changelog with a migration snippet. For how to use the API, see
Fingerprints.
Why#
Version 1 kept every setting twice: a value (screen_width: Option<u32>) and a
mode that said whether to use it (flags.screen_masking: MaskingMode). That
allowed states that mean nothing, and it made callers say one thing in two
places:
MaskingMode::Mask and ::Custom did the same thing in every provider, Natural and Disabled did the same thing, and Auto was added to paper over the rest.
A mode could be on with no value (a generic fill nobody asked for), or a value could be given while the mode was off.
Identity data (a time zone) and injection policy (is toString masked, are permissions granted) lived in the same struct.
The model#
One field per setting, four states:
| State | Means | Where |
|---|---|---|
Natural | The browser’s own value. The default. | Every setting |
Random | A random valid value, coherent with the rest, seeded by .seed(u64). Resolved by build(). | Every setting that has a random value |
Value(T) | Exactly this value. A plain value converts to it. | Every setting |
Off | The API is turned off: unavailable, or denied. | Geolocation (denied), WebRTC (unavailable), media devices (none), battery, Bluetooth, speech voices, network information and Client Hints (unavailable) |
- The value and the mode are one field, so they cannot disagree.
Randomis resolved bybuild(): the builtFingerprintholdsNatural,ValueorOff. A random value is drawn together with the rest of the identity (same operating system, same region, same locale), so it never contradicts a value the caller gave.build()returns an error naming any setting outside the list above that was givenOff.- Behaviour that is not a per-setting value (WebRTC IP policy, canvas and
audio noise, native
toString, granted permissions, blocked ports, …) is aStealthPolicy. A policy preset never changes a setting’sSpoof. - The built
Fingerprinthas private fields and read accessors. To change one,fp.to_builder(), change it,build().
Before and after (public signatures)#
Setting a value#
A plain value is Value. Spoof::Random, Spoof::Natural and Spoof::Off are
accepted everywhere (Off is rejected by build() where it means nothing).
Every other setter follows the same shape: platform, hardware_concurrency,
device_memory, max_touch_points, vendor, locale, languages,
accept_languages, pixel_ratio, color_depth, avail_screen, fonts,
media_devices, proxy, battery, connection, speech_voices, bluetooth,
client_hints.
Policy#
StealthFlags stays as a #[deprecated] alias of StealthPolicy.
Reading and editing#
Removed#
MaskingMode, NoiseMode, PopupMode, ProxyMaskingMode, StartupBehavior,
Fingerprint::fills_defaults, Fingerprint::geolocation_popup, the public
fields of Fingerprint, FingerprintBuilder::{flags, with_flags, canvas_seed, audio_seed}. CanvasNoiseMode becomes CanvasNoise
(Natural, Persistent, Random, Low).
The example that must compile#
The random position is a spot near Berlin, because the time zone is Value.
Random: how it stays coherent#
build() works out the operating system (the explicit one, else the one the
user agent names, else Windows), then the place: the region of a Value time
zone, else of a Value locale, else a drawn one. Every Random setting is taken
from one seeded draw of that machine and that place (stealth::randomize), so a
random locale agrees with a Value time zone, a random position is inside that
time zone’s main city, and a random graphics adapter is one the operating system
reports. build() returns FingerprintError when no coherent value exists (a
random locale next to a time zone the place table does not know).
.seed(u64) seeds the draw and the canvas and audio noise, so the same calls
give the same fingerprint. Without it the draw uses fresh entropy, and the seed
it used is kept in the fingerprint.
JSON#
Spoof<T> serialises as "natural", "random", "off" or the value itself.
Natural fields are left out. null reads as Natural. A fingerprint that
holds "random" is resolved when it is read, the same way build() does it.
ProfileParams (external profile payloads) keeps its string flags, because the
format is another tool’s, and reads the same words for every key:
| Word in the payload | Becomes |
|---|---|
natural, native, pass_through, whitelist | Natural |
auto | the value the payload holds, else Natural |
mask, spoof, custom, generic, random, noise | the value the payload holds, else Random |
off, disabled, block, empty | Off where an API can be off (geolocation, WebRTC, media devices); Natural, logged, elsewhere |
| a literal | Value |
| an unknown word | Natural, logged |
The policy keys (canvas_noise, audio_masking, native_spoofing,
grant_permissions, disable_csp, quic_mode, ports_masking) map to the
policy. The full table is in
Fingerprints: words every key reads.
As built#
Value types
The value types are ScreenSize, Geolocation, WebGl, WebRtcAddresses and
MediaDevices (stealth/settings.rs); each converts from the obvious tuple.
languages and accept_languages stay comma-separated strings, fonts is a
Spoof<Vec<String>>, and Web Bluetooth is a Spoof<bool> (true: an adapter,
false: none, Off: no API).
FingerprintError
FingerprintError has three cases: OffNotAvailable, NoRandomValue (proxy,
oscpu, build_id) and Incoherent.
Seeds
Without .seed(..), a draw uses fresh entropy and a fingerprint with nothing
to draw takes a seed from its user agent, so a preset is the same every time.
Denied geolocation
A denied geolocation (Off) is scripted even under native spoofing, because
no protocol command denies the request for one tab.