use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
const SCREEN_RESOLUTIONS: &[(u32, u32)] = &[
(1920, 1080),
(2560, 1440),
(1440, 900),
(1366, 768),
(1536, 864),
(1280, 800),
(2560, 1600),
(1680, 1050),
];
const TIMEZONES: &[&str] = &[
"America/New_York",
"America/Chicago",
"America/Denver",
"America/Los_Angeles",
"Europe/London",
"Europe/Paris",
"Europe/Berlin",
"Asia/Tokyo",
"Asia/Shanghai",
"Australia/Sydney",
];
const LANGUAGES: &[&str] = &[
"en-US", "en-GB", "en-AU", "en-CA", "fr-FR", "de-DE", "es-ES", "it-IT", "pt-BR", "ja-JP",
"zh-CN",
];
const HARDWARE_CONCURRENCY: &[u32] = &[4, 8, 12, 16];
const DEVICE_MEMORY: &[u32] = &[4, 8, 16];
const WEBGL_PROFILES: &[(&str, &str, &str)] = &[
("Intel Inc.", "Intel Iris OpenGL Engine", "MacIntel"),
("Intel Inc.", "Intel UHD Graphics 630", "MacIntel"),
(
"Google Inc. (Apple)",
"ANGLE (Apple, Apple M2, OpenGL 4.1)",
"MacIntel",
),
(
"Google Inc. (NVIDIA)",
"ANGLE (NVIDIA, NVIDIA GeForce RTX 3080, OpenGL 4.1)",
"Win32",
),
(
"Google Inc. (Intel)",
"ANGLE (Intel, Intel(R) UHD Graphics 770, OpenGL 4.6)",
"Win32",
),
(
"Google Inc. (AMD)",
"ANGLE (AMD, AMD Radeon RX 6800 XT Direct3D11 vs_5_0 ps_5_0)",
"Win32",
),
];
const WINDOWS_WEBGL_PROFILES: &[(&str, &str)] = &[
(
"Google Inc. (NVIDIA)",
"ANGLE (NVIDIA, NVIDIA GeForce RTX 3080, OpenGL 4.1)",
),
(
"Google Inc. (Intel)",
"ANGLE (Intel, Intel(R) UHD Graphics 770, OpenGL 4.6)",
),
(
"Google Inc. (AMD)",
"ANGLE (AMD, AMD Radeon RX 6800 XT Direct3D11 vs_5_0 ps_5_0)",
),
];
const MACOS_WEBGL_PROFILES: &[(&str, &str)] = &[
("Intel Inc.", "Intel Iris OpenGL Engine"),
("Intel Inc.", "Intel UHD Graphics 630"),
("Google Inc. (Apple)", "ANGLE (Apple, Apple M2, OpenGL 4.1)"),
];
const MOBILE_ANDROID_RESOLUTIONS: &[(u32, u32)] =
&[(393, 851), (390, 844), (412, 915), (414, 896), (360, 780)];
const MOBILE_IOS_RESOLUTIONS: &[(u32, u32)] =
&[(390, 844), (393, 852), (375, 667), (414, 896), (428, 926)];
const ANDROID_WEBGL_PROFILES: &[(&str, &str)] = &[
("Qualcomm", "Adreno (TM) 730"),
("ARM", "Mali-G710 MC10"),
(
"Google Inc. (Qualcomm)",
"ANGLE (Qualcomm, Adreno (TM) 730, OpenGL ES 3.2)",
),
("Google Inc. (ARM)", "ANGLE (ARM, Mali-G610, OpenGL ES 3.2)"),
];
const IOS_WEBGL_PROFILES: &[(&str, &str)] = &[
("Apple Inc.", "Apple A16 GPU"),
("Apple Inc.", "Apple A15 GPU"),
("Apple Inc.", "Apple A14 GPU"),
("Apple Inc.", "Apple M1"),
];
const WINDOWS_FONTS: &[&str] = &[
"Arial",
"Calibri",
"Cambria",
"Comic Sans MS",
"Consolas",
"Courier New",
"Georgia",
"Impact",
"Segoe UI",
"Tahoma",
"Times New Roman",
"Trebuchet MS",
"Verdana",
];
const MACOS_FONTS: &[&str] = &[
"Arial",
"Avenir",
"Baskerville",
"Courier New",
"Futura",
"Georgia",
"Helvetica Neue",
"Lucida Grande",
"Optima",
"Palatino",
"Times New Roman",
"Verdana",
];
const LINUX_FONTS: &[&str] = &[
"Arial",
"DejaVu Sans",
"DejaVu Serif",
"FreeMono",
"Liberation Mono",
"Liberation Sans",
"Liberation Serif",
"Times New Roman",
"Ubuntu",
];
const MOBILE_ANDROID_FONTS: &[&str] = &[
"Roboto",
"Noto Sans",
"Droid Sans",
"sans-serif",
"serif",
"monospace",
];
const MOBILE_IOS_FONTS: &[&str] = &[
"Helvetica Neue",
"Arial",
"Georgia",
"Times New Roman",
"Courier New",
];
const CHROME_VERSIONS: &[u32] = &[120, 121, 122, 123, 124, 125];
const EDGE_VERSIONS: &[u32] = &[120, 121, 122, 123, 124];
const FIREFOX_VERSIONS: &[u32] = &[121, 122, 123, 124, 125, 126];
const SAFARI_VERSIONS: &[&str] = &["17.0", "17.1", "17.2", "17.3", "17.4"];
const IOS_OS_VERSIONS: &[&str] = &["16_6", "17_0", "17_1", "17_2", "17_3"];
const fn rng(seed: u64, step: u64) -> u64 {
let x = seed.wrapping_add(step.wrapping_mul(0x9e37_79b9_7f4a_7c15));
let x = (x ^ (x >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
let x = (x ^ (x >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
x ^ (x >> 31)
}
fn pick<T: Copy + Default>(items: &[T], entropy: u64) -> T {
let idx = usize::try_from(entropy).unwrap_or(usize::MAX) % items.len().max(1);
items.get(idx).copied().unwrap_or_default()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Fingerprint {
pub user_agent: String,
pub screen_resolution: (u32, u32),
pub timezone: String,
pub language: String,
pub platform: String,
pub hardware_concurrency: u32,
pub device_memory: u32,
pub webgl_vendor: Option<String>,
pub webgl_renderer: Option<String>,
pub canvas_noise: bool,
pub fonts: Vec<String>,
}
impl Default for Fingerprint {
fn default() -> Self {
Self {
user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) \
AppleWebKit/537.36 (KHTML, like Gecko) \
Chrome/120.0.0.0 Safari/537.36"
.to_string(),
screen_resolution: (1920, 1080),
timezone: "America/New_York".to_string(),
language: "en-US".to_string(),
platform: "MacIntel".to_string(),
hardware_concurrency: 8,
device_memory: 8,
webgl_vendor: Some("Intel Inc.".to_string()),
webgl_renderer: Some("Intel Iris OpenGL Engine".to_string()),
canvas_noise: true,
fonts: vec![],
}
}
}
impl Fingerprint {
pub fn random() -> Self {
let seed = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() ^ u64::from(d.subsec_nanos()))
.unwrap_or(0x5a5a_5a5a_5a5a_5a5a);
let res = pick(SCREEN_RESOLUTIONS, rng(seed, 1));
let tz = pick(TIMEZONES, rng(seed, 2));
let lang = pick(LANGUAGES, rng(seed, 3));
let hw = pick(HARDWARE_CONCURRENCY, rng(seed, 4));
let dm = pick(DEVICE_MEMORY, rng(seed, 5));
let (wv, wr, platform) = pick(WEBGL_PROFILES, rng(seed, 6));
let user_agent = if platform == "Win32" {
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
AppleWebKit/537.36 (KHTML, like Gecko) \
Chrome/120.0.0.0 Safari/537.36"
.to_string()
} else {
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) \
AppleWebKit/537.36 (KHTML, like Gecko) \
Chrome/120.0.0.0 Safari/537.36"
.to_string()
};
let fonts: Vec<String> = if platform == "Win32" {
WINDOWS_FONTS.iter().map(|s| (*s).to_string()).collect()
} else {
MACOS_FONTS.iter().map(|s| (*s).to_string()).collect()
};
Self {
user_agent,
screen_resolution: res,
timezone: tz.to_string(),
language: lang.to_string(),
platform: platform.to_string(),
hardware_concurrency: hw,
device_memory: dm,
webgl_vendor: Some(wv.to_string()),
webgl_renderer: Some(wr.to_string()),
canvas_noise: true,
fonts,
}
}
pub fn from_profile(profile: &FingerprintProfile) -> Self {
profile.fingerprint.clone()
}
pub fn from_device_profile(device: DeviceProfile, seed: u64) -> Self {
match device {
DeviceProfile::DesktopWindows => Self::for_windows(seed),
DeviceProfile::DesktopMac => Self::for_mac(seed),
DeviceProfile::DesktopLinux => Self::for_linux(seed),
DeviceProfile::MobileAndroid => Self::for_android(seed),
DeviceProfile::MobileIOS => Self::for_ios(seed),
}
}
pub fn validate_consistency(&self) -> Vec<String> {
let mut issues = Vec::new();
if self.platform == "Win32" && self.user_agent.contains("Mac OS X") {
issues.push("Win32 platform but user-agent says Mac OS X".to_string());
}
if self.platform == "MacIntel" && self.user_agent.contains("Windows NT") {
issues.push("MacIntel platform but user-agent says Windows NT".to_string());
}
if self.platform.starts_with("Linux") && self.user_agent.contains("Windows NT") {
issues.push("Linux platform but user-agent says Windows NT".to_string());
}
if let Some(vendor) = &self.webgl_vendor {
if (self.platform == "Win32" || self.platform == "MacIntel")
&& (vendor.contains("Qualcomm")
|| vendor.contains("Adreno")
|| vendor.contains("Mali"))
{
issues.push(format!(
"Desktop platform '{}' has mobile GPU vendor '{vendor}'",
self.platform
));
}
if self.platform == "Win32" && vendor.starts_with("Apple") {
issues.push(format!("Win32 platform has Apple GPU vendor '{vendor}'"));
}
}
if !self.fonts.is_empty() {
let has_win_exclusive = self
.fonts
.iter()
.any(|f| matches!(f.as_str(), "Segoe UI" | "Calibri" | "Consolas" | "Tahoma"));
let has_mac_exclusive = self.fonts.iter().any(|f| {
matches!(
f.as_str(),
"Lucida Grande" | "Avenir" | "Optima" | "Futura" | "Baskerville"
)
});
let has_linux_exclusive = self.fonts.iter().any(|f| {
matches!(
f.as_str(),
"DejaVu Sans" | "Liberation Sans" | "Ubuntu" | "FreeMono"
)
});
if self.platform == "MacIntel" && has_win_exclusive {
issues.push("MacIntel platform has Windows-exclusive fonts".to_string());
}
if self.platform == "Win32" && has_mac_exclusive {
issues.push("Win32 platform has macOS-exclusive fonts".to_string());
}
if self.platform == "Win32" && has_linux_exclusive {
issues.push("Win32 platform has Linux-exclusive fonts".to_string());
}
}
issues
}
fn for_windows(seed: u64) -> Self {
let browser = BrowserKind::for_device(DeviceProfile::DesktopWindows, seed);
let user_agent = match browser {
BrowserKind::Chrome | BrowserKind::Safari => {
let ver = pick(CHROME_VERSIONS, rng(seed, 10));
format!(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
AppleWebKit/537.36 (KHTML, like Gecko) \
Chrome/{ver}.0.0.0 Safari/537.36"
)
}
BrowserKind::Edge => {
let ver = pick(EDGE_VERSIONS, rng(seed, 10));
format!(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
AppleWebKit/537.36 (KHTML, like Gecko) \
Chrome/{ver}.0.0.0 Safari/537.36 Edg/{ver}.0.0.0"
)
}
BrowserKind::Firefox => {
let ver = pick(FIREFOX_VERSIONS, rng(seed, 10));
format!(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:{ver}.0) \
Gecko/20100101 Firefox/{ver}.0"
)
}
};
let (webgl_vendor, webgl_renderer) = pick(WINDOWS_WEBGL_PROFILES, rng(seed, 7));
let fonts = WINDOWS_FONTS.iter().map(|s| (*s).to_string()).collect();
Self {
user_agent,
screen_resolution: pick(SCREEN_RESOLUTIONS, rng(seed, 1)),
timezone: pick(TIMEZONES, rng(seed, 2)).to_string(),
language: pick(LANGUAGES, rng(seed, 3)).to_string(),
platform: "Win32".to_string(),
hardware_concurrency: pick(HARDWARE_CONCURRENCY, rng(seed, 4)),
device_memory: pick(DEVICE_MEMORY, rng(seed, 5)),
webgl_vendor: Some(webgl_vendor.to_string()),
webgl_renderer: Some(webgl_renderer.to_string()),
canvas_noise: true,
fonts,
}
}
fn for_mac(seed: u64) -> Self {
let browser = BrowserKind::for_device(DeviceProfile::DesktopMac, seed);
let user_agent = match browser {
BrowserKind::Chrome | BrowserKind::Edge => {
let ver = pick(CHROME_VERSIONS, rng(seed, 10));
format!(
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) \
AppleWebKit/537.36 (KHTML, like Gecko) \
Chrome/{ver}.0.0.0 Safari/537.36"
)
}
BrowserKind::Safari => {
let ver = pick(SAFARI_VERSIONS, rng(seed, 10));
format!(
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) \
AppleWebKit/605.1.15 (KHTML, like Gecko) \
Version/{ver} Safari/605.1.15"
)
}
BrowserKind::Firefox => {
let ver = pick(FIREFOX_VERSIONS, rng(seed, 10));
format!(
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:{ver}.0) \
Gecko/20100101 Firefox/{ver}.0"
)
}
};
let (webgl_vendor, webgl_renderer) = pick(MACOS_WEBGL_PROFILES, rng(seed, 7));
let fonts = MACOS_FONTS.iter().map(|s| (*s).to_string()).collect();
Self {
user_agent,
screen_resolution: pick(SCREEN_RESOLUTIONS, rng(seed, 1)),
timezone: pick(TIMEZONES, rng(seed, 2)).to_string(),
language: pick(LANGUAGES, rng(seed, 3)).to_string(),
platform: "MacIntel".to_string(),
hardware_concurrency: pick(HARDWARE_CONCURRENCY, rng(seed, 4)),
device_memory: pick(DEVICE_MEMORY, rng(seed, 5)),
webgl_vendor: Some(webgl_vendor.to_string()),
webgl_renderer: Some(webgl_renderer.to_string()),
canvas_noise: true,
fonts,
}
}
fn for_linux(seed: u64) -> Self {
let browser = BrowserKind::for_device(DeviceProfile::DesktopLinux, seed);
let user_agent = if browser == BrowserKind::Firefox {
let ver = pick(FIREFOX_VERSIONS, rng(seed, 10));
format!(
"Mozilla/5.0 (X11; Linux x86_64; rv:{ver}.0) \
Gecko/20100101 Firefox/{ver}.0"
)
} else {
let ver = pick(CHROME_VERSIONS, rng(seed, 10));
format!(
"Mozilla/5.0 (X11; Linux x86_64) \
AppleWebKit/537.36 (KHTML, like Gecko) \
Chrome/{ver}.0.0.0 Safari/537.36"
)
};
let fonts = LINUX_FONTS.iter().map(|s| (*s).to_string()).collect();
Self {
user_agent,
screen_resolution: pick(SCREEN_RESOLUTIONS, rng(seed, 1)),
timezone: pick(TIMEZONES, rng(seed, 2)).to_string(),
language: pick(LANGUAGES, rng(seed, 3)).to_string(),
platform: "Linux x86_64".to_string(),
hardware_concurrency: pick(HARDWARE_CONCURRENCY, rng(seed, 4)),
device_memory: pick(DEVICE_MEMORY, rng(seed, 5)),
webgl_vendor: Some("Mesa/X.org".to_string()),
webgl_renderer: Some("llvmpipe (LLVM 15.0.7, 256 bits)".to_string()),
canvas_noise: true,
fonts,
}
}
fn for_android(seed: u64) -> Self {
let browser = BrowserKind::for_device(DeviceProfile::MobileAndroid, seed);
let user_agent = if browser == BrowserKind::Firefox {
let ver = pick(FIREFOX_VERSIONS, rng(seed, 10));
format!(
"Mozilla/5.0 (Android 14; Mobile; rv:{ver}.0) \
Gecko/20100101 Firefox/{ver}.0"
)
} else {
let ver = pick(CHROME_VERSIONS, rng(seed, 10));
format!(
"Mozilla/5.0 (Linux; Android 14; Pixel 7) \
AppleWebKit/537.36 (KHTML, like Gecko) \
Chrome/{ver}.0.6099.144 Mobile Safari/537.36"
)
};
let (webgl_vendor, webgl_renderer) = pick(ANDROID_WEBGL_PROFILES, rng(seed, 6));
let fonts = MOBILE_ANDROID_FONTS
.iter()
.map(|s| (*s).to_string())
.collect();
Self {
user_agent,
screen_resolution: pick(MOBILE_ANDROID_RESOLUTIONS, rng(seed, 1)),
timezone: pick(TIMEZONES, rng(seed, 2)).to_string(),
language: pick(LANGUAGES, rng(seed, 3)).to_string(),
platform: "Linux armv8l".to_string(),
hardware_concurrency: pick(HARDWARE_CONCURRENCY, rng(seed, 4)),
device_memory: pick(DEVICE_MEMORY, rng(seed, 5)),
webgl_vendor: Some(webgl_vendor.to_string()),
webgl_renderer: Some(webgl_renderer.to_string()),
canvas_noise: true,
fonts,
}
}
fn for_ios(seed: u64) -> Self {
let safari_ver = pick(SAFARI_VERSIONS, rng(seed, 10));
let ios_ver = pick(IOS_OS_VERSIONS, rng(seed, 11));
let user_agent = format!(
"Mozilla/5.0 (iPhone; CPU iPhone OS {ios_ver} like Mac OS X) \
AppleWebKit/605.1.15 (KHTML, like Gecko) \
Version/{safari_ver} Mobile/15E148 Safari/604.1"
);
let (webgl_vendor, webgl_renderer) = pick(IOS_WEBGL_PROFILES, rng(seed, 6));
let fonts = MOBILE_IOS_FONTS.iter().map(|s| (*s).to_string()).collect();
Self {
user_agent,
screen_resolution: pick(MOBILE_IOS_RESOLUTIONS, rng(seed, 1)),
timezone: pick(TIMEZONES, rng(seed, 2)).to_string(),
language: pick(LANGUAGES, rng(seed, 3)).to_string(),
platform: "iPhone".to_string(),
hardware_concurrency: 6,
device_memory: 4,
webgl_vendor: Some(webgl_vendor.to_string()),
webgl_renderer: Some(webgl_renderer.to_string()),
canvas_noise: true,
fonts,
}
}
pub fn injection_script(&self) -> String {
let mut parts = vec![
screen_script(self.screen_resolution),
timezone_script(&self.timezone),
language_script(&self.language, &self.user_agent),
hardware_script(self.hardware_concurrency, self.device_memory),
];
if let (Some(vendor), Some(renderer)) = (&self.webgl_vendor, &self.webgl_renderer) {
parts.push(webgl_script(vendor, renderer));
}
if self.canvas_noise {
parts.push(canvas_noise_script());
}
parts.push(audio_fingerprint_script());
parts.push(connection_spoof_script());
parts.push(font_measurement_intercept_script());
parts.push(storage_estimate_spoof_script());
parts.push(battery_spoof_script());
parts.push(plugins_spoof_script());
format!("(function() {{\n{}\n}})();", parts.join("\n\n"))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FingerprintProfile {
pub name: String,
pub fingerprint: Fingerprint,
}
impl FingerprintProfile {
pub fn new(name: String) -> Self {
Self {
name,
fingerprint: Fingerprint::random(),
}
}
pub fn random_weighted(name: String) -> Self {
let seed = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() ^ u64::from(d.subsec_nanos()))
.unwrap_or(0x5a5a_5a5a_5a5a_5a5a);
let device = DeviceProfile::random_weighted(seed);
Self {
name,
fingerprint: Fingerprint::from_device_profile(device, seed),
}
}
}
pub fn inject_fingerprint(fingerprint: &Fingerprint) -> String {
fingerprint.injection_script()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum DeviceProfile {
#[default]
DesktopWindows,
DesktopMac,
DesktopLinux,
MobileAndroid,
MobileIOS,
}
impl DeviceProfile {
pub const fn random_weighted(seed: u64) -> Self {
let v = rng(seed, 97) % 100;
match v {
0..=69 => Self::DesktopWindows,
70..=89 => Self::DesktopMac,
_ => Self::DesktopLinux,
}
}
pub const fn is_mobile(self) -> bool {
matches!(self, Self::MobileAndroid | Self::MobileIOS)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum BrowserKind {
#[default]
Chrome,
Edge,
Safari,
Firefox,
}
impl BrowserKind {
pub const fn for_device(device: DeviceProfile, seed: u64) -> Self {
match device {
DeviceProfile::MobileIOS => Self::Safari,
DeviceProfile::MobileAndroid => {
let v = rng(seed, 201) % 100;
if v < 90 { Self::Chrome } else { Self::Firefox }
}
DeviceProfile::DesktopMac => {
let v = rng(seed, 201) % 100;
match v {
0..=55 => Self::Chrome,
56..=91 => Self::Safari,
_ => Self::Firefox,
}
}
_ => {
let v = rng(seed, 201) % 100;
match v {
0..=64 => Self::Chrome,
65..=80 => Self::Edge,
_ => Self::Firefox,
}
}
}
}
}
fn screen_script((width, height): (u32, u32)) -> String {
let avail_height = height.saturating_sub(40);
format!(
r" // Screen dimensions
const _defineScreen = (prop, val) =>
Object.defineProperty(screen, prop, {{ get: () => val, configurable: false }});
_defineScreen('width', {width});
_defineScreen('height', {height});
_defineScreen('availWidth', {width});
_defineScreen('availHeight', {avail_height});
_defineScreen('availLeft', 0);
_defineScreen('availTop', 0);
_defineScreen('colorDepth', 24);
_defineScreen('pixelDepth', 24);
// outerWidth/outerHeight: headless Chrome may return 0; spoof to viewport size.
try {{
Object.defineProperty(window, 'outerWidth', {{ get: () => {width}, configurable: true }});
Object.defineProperty(window, 'outerHeight', {{ get: () => {height}, configurable: true }});
}} catch(_) {{}}"
)
}
fn timezone_script(timezone: &str) -> String {
format!(
r" // Timezone via Intl.DateTimeFormat
const _origResolvedOptions = Intl.DateTimeFormat.prototype.resolvedOptions;
Intl.DateTimeFormat.prototype.resolvedOptions = function() {{
const opts = _origResolvedOptions.apply(this, arguments);
opts.timeZone = {timezone:?};
return opts;
}};"
)
}
fn language_script(language: &str, user_agent: &str) -> String {
let primary = language.split('-').next().unwrap_or("en");
format!(
r" // Language + userAgent
Object.defineProperty(navigator, 'language', {{ get: () => {language:?}, configurable: false }});
Object.defineProperty(navigator, 'languages', {{ get: () => Object.freeze([{language:?}, {primary:?}]), configurable: false }});
Object.defineProperty(navigator, 'userAgent', {{ get: () => {user_agent:?}, configurable: false }});"
)
}
fn hardware_script(concurrency: u32, memory: u32) -> String {
format!(
r" // Hardware concurrency + device memory
Object.defineProperty(navigator, 'hardwareConcurrency', {{ get: () => {concurrency}, configurable: false }});
Object.defineProperty(navigator, 'deviceMemory', {{ get: () => {memory}, configurable: false }});"
)
}
fn webgl_script(vendor: &str, renderer: &str) -> String {
format!(
r" // WebGL vendor + renderer
(function() {{
const _getContext = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype.getContext = function(type, attrs) {{
const ctx = _getContext.call(this, type, attrs);
if (!ctx) return ctx;
if (type === 'webgl' || type === 'webgl2' || type === 'experimental-webgl') {{
const _getParam = ctx.getParameter.bind(ctx);
ctx.getParameter = function(param) {{
if (param === 0x1F00) return {vendor:?}; // GL_VENDOR
if (param === 0x1F01) return {renderer:?}; // GL_RENDERER
return _getParam(param);
}};
}}
return ctx;
}};
}})();"
)
}
fn canvas_noise_script() -> String {
r" // Canvas noise: flip lowest bit of R/G/B channels to defeat pixel readback
(function() {
const _getImageData = CanvasRenderingContext2D.prototype.getImageData;
CanvasRenderingContext2D.prototype.getImageData = function() {
const id = _getImageData.apply(this, arguments);
const d = id.data;
for (let i = 0; i < d.length; i += 4) {
d[i] ^= 1;
d[i + 1] ^= 1;
d[i + 2] ^= 1;
}
return id;
};
})();"
.to_string()
}
fn audio_fingerprint_script() -> String {
r" // Audio fingerprint defence: add sub-epsilon noise to frequency data
(function() {
if (typeof AnalyserNode === 'undefined') return;
const _getFloatFreq = AnalyserNode.prototype.getFloatFrequencyData;
AnalyserNode.prototype.getFloatFrequencyData = function(arr) {
_getFloatFreq.apply(this, arguments);
for (let i = 0; i < arr.length; i++) {
arr[i] += (Math.random() - 0.5) * 1e-7;
}
};
})();"
.to_string()
}
fn connection_spoof_script() -> String {
r" // NetworkInformation API spoof (navigator.connection)
(function() {
const _seed = Math.floor(performance.timeOrigin % 997);
const conn = {
rtt: 50 + _seed % 100,
downlink: 5 + _seed % 15,
effectiveType: '4g',
type: 'wifi',
saveData: false,
onchange: null,
ontypechange: null,
addEventListener: function() {},
removeEventListener: function() {},
dispatchEvent: function() { return true; },
};
try {
Object.defineProperty(navigator, 'connection', {
get: () => conn,
enumerable: true,
configurable: false,
});
} catch (_) {}
})();"
.to_string()
}
fn font_measurement_intercept_script() -> String {
r" // getBoundingClientRect font-probe intercept (Turnstile Layer 1)
(function() {
const _origGBCR = Element.prototype.getBoundingClientRect;
const _seed = Math.floor(performance.timeOrigin % 9973);
function _jitter(base, range) {
return base + ((_seed * 1103515245 + 12345) & 0x7fffffff) % range;
}
Element.prototype.getBoundingClientRect = function() {
const rect = _origGBCR.call(this);
// Only intercept zero-size rects on hidden probe elements (the font-
// measurement pattern: position absolute/fixed, visibility hidden).
if (rect.width === 0 && rect.height === 0) {
const st = window.getComputedStyle(this);
const vis = st.getPropertyValue('visibility');
const pos = st.getPropertyValue('position');
const ariaHidden = this.getAttribute('aria-hidden');
if ((vis === 'hidden' || ariaHidden === 'true') &&
(pos === 'absolute' || pos === 'fixed')) {
const w = _jitter(10, 8);
const h = _jitter(14, 4);
return new DOMRect(0, 0, w, h);
}
}
return rect;
};
})();"
.to_string()
}
fn storage_estimate_spoof_script() -> String {
r" // navigator.storage.estimate() spoof (Turnstile Layer 1 — storage)
(function() {
if (!navigator.storage || typeof navigator.storage.estimate !== 'function') return;
const _origEstimate = navigator.storage.estimate.bind(navigator.storage);
const _seed = Math.floor(performance.timeOrigin % 9973);
// Realistic Chrome profile: ~250 GB quota, small stable usage.
const quota = (240 + _seed % 20) * 1073741824;
const usage = (5 + _seed % 10) * 1048576;
navigator.storage.estimate = function() {
return _origEstimate().then(function(result) {
return Object.assign({}, result, {
quota: quota,
usage: usage
});
});
};
})();"
.to_string()
}
fn plugins_spoof_script() -> String {
r" // navigator.plugins / mimeTypes — empty array = instant headless flag
(function() {
// Build minimal objects that survive instanceof checks.
var mime0 = { type: 'application/pdf', description: 'Portable Document Format', suffixes: 'pdf', enabledPlugin: null };
var mime1 = { type: 'text/pdf', description: 'Portable Document Format', suffixes: 'pdf', enabledPlugin: null };
var pdfPlugin = {
name: 'PDF Viewer',
description: 'Portable Document Format',
filename: 'internal-pdf-viewer',
length: 2,
0: mime0, 1: mime1,
item: function(i) { return [mime0, mime1][i] || null; },
namedItem: function(n) {
if (n === 'application/pdf') return mime0;
if (n === 'text/pdf') return mime1;
return null;
},
};
mime0.enabledPlugin = pdfPlugin;
mime1.enabledPlugin = pdfPlugin;
var fakePlugins = {
length: 1,
0: pdfPlugin,
item: function(i) { return i === 0 ? pdfPlugin : null; },
namedItem: function(n) { return n === 'PDF Viewer' ? pdfPlugin : null; },
refresh: function() {},
};
var fakeMimes = {
length: 2,
0: mime0, 1: mime1,
item: function(i) { return [mime0, mime1][i] || null; },
namedItem: function(n) {
if (n === 'application/pdf') return mime0;
if (n === 'text/pdf') return mime1;
return null;
},
};
try {
Object.defineProperty(navigator, 'plugins', { get: function() { return fakePlugins; }, configurable: false });
Object.defineProperty(navigator, 'mimeTypes', { get: function() { return fakeMimes; }, configurable: false });
} catch(_) {}
})();"
.to_string()
}
fn battery_spoof_script() -> String {
r" // Battery API normalization (navigator.getBattery)
(function() {
if (typeof navigator.getBattery !== 'function') return;
const _seed = Math.floor(performance.timeOrigin % 997);
const battery = {
charging: false,
chargingTime: Infinity,
dischargingTime: 3600 + _seed * 7,
level: 0.65 + (_seed % 30) / 100,
onchargingchange: null,
onchargingtimechange: null,
ondischargingtimechange: null,
onlevelchange: null,
addEventListener: function() {},
removeEventListener: function() {},
dispatchEvent: function() { return true; },
};
navigator.getBattery = function() {
return Promise.resolve(battery);
};
})();"
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn random_fingerprint_has_valid_ranges() {
let fp = Fingerprint::random();
let (w, h) = fp.screen_resolution;
assert!(
(1280..=3840).contains(&w),
"width {w} out of expected range"
);
assert!(
(768..=2160).contains(&h),
"height {h} out of expected range"
);
assert!(
HARDWARE_CONCURRENCY.contains(&fp.hardware_concurrency),
"hardware_concurrency {} not in pool",
fp.hardware_concurrency
);
assert!(
DEVICE_MEMORY.contains(&fp.device_memory),
"device_memory {} not in pool",
fp.device_memory
);
assert!(
TIMEZONES.contains(&fp.timezone.as_str()),
"timezone {} not in pool",
fp.timezone
);
assert!(
LANGUAGES.contains(&fp.language.as_str()),
"language {} not in pool",
fp.language
);
}
#[test]
fn random_generates_different_values_over_time() {
let fp1 = Fingerprint::random();
let fp2 = Fingerprint::random();
assert!(!fp1.user_agent.is_empty());
assert!(!fp2.user_agent.is_empty());
}
#[test]
fn injection_script_contains_screen_dimensions() {
let fp = Fingerprint {
screen_resolution: (2560, 1440),
..Fingerprint::default()
};
let script = fp.injection_script();
assert!(script.contains("2560"), "missing width in script");
assert!(script.contains("1440"), "missing height in script");
}
#[test]
fn injection_script_contains_timezone() {
let fp = Fingerprint {
timezone: "Europe/Berlin".to_string(),
..Fingerprint::default()
};
let script = fp.injection_script();
assert!(script.contains("Europe/Berlin"), "timezone missing");
}
#[test]
fn injection_script_contains_canvas_noise_when_enabled() {
let fp = Fingerprint {
canvas_noise: true,
..Fingerprint::default()
};
let script = fp.injection_script();
assert!(
script.contains("getImageData"),
"canvas noise block missing"
);
}
#[test]
fn injection_script_omits_canvas_noise_when_disabled() {
let fp = Fingerprint {
canvas_noise: false,
..Fingerprint::default()
};
let script = fp.injection_script();
assert!(
!script.contains("getImageData"),
"canvas noise should be absent"
);
}
#[test]
fn injection_script_contains_webgl_vendor() {
let fp = Fingerprint {
webgl_vendor: Some("TestVendor".to_string()),
webgl_renderer: Some("TestRenderer".to_string()),
canvas_noise: false,
..Fingerprint::default()
};
let script = fp.injection_script();
assert!(script.contains("TestVendor"), "WebGL vendor missing");
assert!(script.contains("TestRenderer"), "WebGL renderer missing");
}
#[test]
fn inject_fingerprint_fn_equals_method() {
let fp = Fingerprint::default();
assert_eq!(inject_fingerprint(&fp), fp.injection_script());
}
#[test]
fn from_profile_returns_profile_fingerprint() {
let profile = FingerprintProfile::new("test".to_string());
let fp = Fingerprint::from_profile(&profile);
assert_eq!(fp.user_agent, profile.fingerprint.user_agent);
}
#[test]
fn script_is_wrapped_in_iife() {
let script = Fingerprint::default().injection_script();
assert!(script.starts_with("(function()"), "must start with IIFE");
assert!(script.ends_with("})();"), "must end with IIFE call");
}
#[test]
fn rng_produces_distinct_values_for_different_steps() {
let seed = 0xdead_beef_cafe_babe_u64;
let v1 = rng(seed, 1);
let v2 = rng(seed, 2);
let v3 = rng(seed, 3);
assert_ne!(v1, v2);
assert_ne!(v2, v3);
}
#[test]
fn device_profile_windows_is_consistent() {
let fp = Fingerprint::from_device_profile(DeviceProfile::DesktopWindows, 42);
assert_eq!(fp.platform, "Win32");
assert!(fp.user_agent.contains("Windows NT"), "UA must be Windows");
assert!(!fp.fonts.is_empty(), "Windows profile must have fonts");
assert!(
fp.validate_consistency().is_empty(),
"must pass consistency"
);
}
#[test]
fn device_profile_mac_is_consistent() {
let fp = Fingerprint::from_device_profile(DeviceProfile::DesktopMac, 42);
assert_eq!(fp.platform, "MacIntel");
assert!(
fp.user_agent.contains("Mac OS X"),
"UA must be macOS: {}",
fp.user_agent
);
assert!(!fp.fonts.is_empty(), "Mac profile must have fonts");
assert!(
fp.validate_consistency().is_empty(),
"must pass consistency"
);
}
#[test]
fn device_profile_linux_is_consistent() {
let fp = Fingerprint::from_device_profile(DeviceProfile::DesktopLinux, 42);
assert_eq!(fp.platform, "Linux x86_64");
assert!(fp.user_agent.contains("Linux"), "UA must be Linux");
assert!(!fp.fonts.is_empty(), "Linux profile must have fonts");
assert!(
fp.validate_consistency().is_empty(),
"must pass consistency"
);
}
#[test]
fn device_profile_android_is_mobile() {
let fp = Fingerprint::from_device_profile(DeviceProfile::MobileAndroid, 42);
assert!(
fp.platform.starts_with("Linux"),
"Android platform should be Linux-based"
);
assert!(
fp.user_agent.contains("Android") || fp.user_agent.contains("Firefox"),
"Android UA mismatch: {}",
fp.user_agent
);
assert!(!fp.fonts.is_empty());
assert!(DeviceProfile::MobileAndroid.is_mobile());
}
#[test]
fn device_profile_ios_is_mobile() {
let fp = Fingerprint::from_device_profile(DeviceProfile::MobileIOS, 42);
assert_eq!(fp.platform, "iPhone");
assert!(
fp.user_agent.contains("iPhone"),
"iOS UA must contain iPhone"
);
assert!(!fp.fonts.is_empty());
assert!(DeviceProfile::MobileIOS.is_mobile());
}
#[test]
fn desktop_profiles_are_not_mobile() {
assert!(!DeviceProfile::DesktopWindows.is_mobile());
assert!(!DeviceProfile::DesktopMac.is_mobile());
assert!(!DeviceProfile::DesktopLinux.is_mobile());
}
#[test]
fn browser_kind_ios_always_safari() {
for seed in [0u64, 1, 42, 999, u64::MAX] {
assert_eq!(
BrowserKind::for_device(DeviceProfile::MobileIOS, seed),
BrowserKind::Safari,
"iOS must always return Safari (seed={seed})"
);
}
}
#[test]
fn device_profile_random_weighted_distribution() {
let windows_count = (0u64..1000)
.filter(|&i| {
DeviceProfile::random_weighted(i * 13 + 7) == DeviceProfile::DesktopWindows
})
.count();
assert!(
windows_count >= 500,
"Expected ≥50% Windows, got {windows_count}/1000"
);
}
#[test]
fn validate_consistency_catches_platform_ua_mismatch() {
let fp = Fingerprint {
platform: "Win32".to_string(),
user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) \
AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36"
.to_string(),
..Fingerprint::default()
};
let issues = fp.validate_consistency();
assert!(!issues.is_empty(), "should detect Win32+Mac UA mismatch");
}
#[test]
fn validate_consistency_catches_platform_font_mismatch() {
let fp = Fingerprint {
platform: "MacIntel".to_string(),
fonts: vec!["Segoe UI".to_string(), "Calibri".to_string()],
..Fingerprint::default()
};
let issues = fp.validate_consistency();
assert!(
!issues.is_empty(),
"should detect MacIntel + Windows fonts mismatch"
);
}
#[test]
fn validate_consistency_passes_for_default() {
let fp = Fingerprint::default();
assert!(fp.validate_consistency().is_empty());
}
#[test]
fn fingerprint_profile_random_weighted_has_fonts() {
let profile = FingerprintProfile::random_weighted("sess-1".to_string());
assert_eq!(profile.name, "sess-1");
assert!(!profile.fingerprint.fonts.is_empty());
assert!(profile.fingerprint.validate_consistency().is_empty());
}
#[test]
fn from_device_profile_serializes_to_json() -> Result<(), Box<dyn std::error::Error>> {
let fp = Fingerprint::from_device_profile(DeviceProfile::DesktopWindows, 123);
let json = serde_json::to_string(&fp)?;
let back: Fingerprint = serde_json::from_str(&json)?;
assert_eq!(back.platform, fp.platform);
assert_eq!(back.fonts, fp.fonts);
Ok(())
}
proptest::proptest! {
#[test]
fn prop_seeded_fingerprint_always_consistent(seed in 0u64..10_000) {
let profile = DeviceProfile::random_weighted(seed);
let fp = Fingerprint::from_device_profile(profile, seed);
let issues = fp.validate_consistency();
proptest::prop_assert!(
issues.is_empty(),
"validate_consistency() failed for seed {seed}: {issues:?}"
);
}
#[test]
fn prop_hardware_concurrency_is_sensible(_seed in 0u64..10_000) {
let fp = Fingerprint::random();
proptest::prop_assert!(
fp.hardware_concurrency >= 1 && fp.hardware_concurrency <= 32,
"hardware_concurrency {} out of [1,32]", fp.hardware_concurrency
);
}
#[test]
fn prop_device_memory_is_valid_value(_seed in 0u64..10_000) {
let fp = Fingerprint::random();
let valid: &[u32] = &[4, 8, 16];
proptest::prop_assert!(
valid.contains(&fp.device_memory),
"device_memory {} is not a valid value", fp.device_memory
);
}
#[test]
fn prop_screen_dimensions_are_plausible(_seed in 0u64..10_000) {
let fp = Fingerprint::random();
let (w, h) = fp.screen_resolution;
proptest::prop_assert!((320..=7680).contains(&w));
proptest::prop_assert!((240..=4320).contains(&h));
}
#[test]
fn prop_fingerprint_profile_passes_consistency(name in "[a-z][a-z0-9]{0,31}") {
let profile = FingerprintProfile::random_weighted(name.clone());
let issues = profile.fingerprint.validate_consistency();
proptest::prop_assert!(
issues.is_empty(),
"FingerprintProfile for '{name}' has issues: {issues:?}"
);
}
#[test]
fn prop_injection_script_non_empty(_seed in 0u64..10_000) {
let fp = Fingerprint::random();
let script = inject_fingerprint(&fp);
proptest::prop_assert!(!script.is_empty());
proptest::prop_assert!(script.contains("navigator"));
}
}
}