Skip to main content

bimp_persona/
fingerprint.rs

1use serde::{Deserialize, Serialize};
2
3use crate::options::Viewport;
4use crate::seed::{BrowserPersonaPreset, PersonaSeed};
5
6/// Network-visible browser fingerprint values.
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub struct NetworkFingerprint {
9    pub user_agent: String,
10    pub accept_language: String,
11    pub accept_header: String,
12    pub sec_ch_ua: String,
13    pub sec_ch_ua_mobile: String,
14    pub sec_ch_ua_platform: String,
15}
16
17/// Automation exposure policy for WebDriver-related JS surfaces.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct AutomationPolicy {
20    pub webdriver: bool,
21    pub expose_webdriver_helpers: bool,
22    pub expose_automation_globals: bool,
23}
24
25impl Default for AutomationPolicy {
26    fn default() -> Self {
27        Self {
28            webdriver: false,
29            expose_webdriver_helpers: false,
30            expose_automation_globals: false,
31        }
32    }
33}
34
35/// Resolved screen fingerprint values.
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37pub struct ScreenFingerprint {
38    pub width: u32,
39    pub height: u32,
40    pub avail_width: u32,
41    pub avail_height: u32,
42    pub avail_left: i32,
43    pub avail_top: i32,
44    pub left: i32,
45    pub top: i32,
46    pub color_depth: u32,
47    pub pixel_depth: u32,
48    pub device_scale_factor: u32,
49    pub is_extended: bool,
50    pub orientation_type: String,
51    pub orientation_angle: u16,
52}
53
54/// Resolved window fingerprint values.
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub struct WindowFingerprint {
57    pub outer_width: u32,
58    pub outer_height: u32,
59    pub screen_x: i32,
60    pub screen_y: i32,
61}
62
63/// Resolved CSS feature-detection fingerprint values.
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub struct CssFingerprint {
66    pub moz_prefix_enabled: bool,
67    pub webkit_prefix_enabled: bool,
68}
69
70impl WindowFingerprint {
71    pub fn from_viewport(viewport: &Viewport) -> Self {
72        Self {
73            outer_width: viewport.width,
74            outer_height: viewport.height,
75            screen_x: 0,
76            screen_y: 0,
77        }
78    }
79}
80
81impl ScreenFingerprint {
82    pub fn from_viewport(viewport: &Viewport) -> Self {
83        Self {
84            width: viewport.width,
85            height: viewport.height,
86            avail_width: viewport.width,
87            avail_height: viewport.height,
88            avail_left: 0,
89            avail_top: 0,
90            left: 0,
91            top: 0,
92            color_depth: 24,
93            pixel_depth: 24,
94            device_scale_factor: viewport.device_scale_factor,
95            is_extended: false,
96            orientation_type: if viewport.width >= viewport.height {
97                "landscape-primary"
98            } else {
99                "portrait-primary"
100            }
101            .to_string(),
102            orientation_angle: 0,
103        }
104    }
105}
106
107/// Resolved WebGL and graphics fingerprint values.
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109pub struct GraphicsFingerprint {
110    pub webgl_vendor: String,
111    pub webgl_renderer: String,
112    pub webgl_masked_vendor: String,
113    pub webgl_masked_renderer: String,
114    pub webgl_seed: PersonaSeed,
115}
116
117/// Resolved JavaScript environment fingerprint values.
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119pub struct JsFingerprint {
120    pub app_version: String,
121    pub platform: String,
122    pub language: String,
123    pub languages: Vec<String>,
124    pub hardware_concurrency: u32,
125    pub device_memory_gb: u32,
126    pub max_touch_points: u32,
127    pub do_not_track: String,
128    pub expose_global_privacy_control: bool,
129    pub global_privacy_control: bool,
130    pub permissions_enabled: bool,
131    pub bluetooth_enabled: bool,
132    pub bluetooth_available: bool,
133    pub media_devices_enabled: bool,
134    pub webgpu_enabled: bool,
135    pub offscreen_canvas_enabled: bool,
136    pub service_worker_enabled: bool,
137    pub ua_platform_version: String,
138    pub ua_architecture: String,
139    pub ua_bitness: String,
140    pub ua_model: String,
141    pub timezone: String,
142    pub vendor: String,
143    pub product_sub: String,
144    pub pdf_viewer_enabled: bool,
145}
146
147/// Resolved locale, timezone, and geolocation fingerprint values.
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149pub struct GeoFingerprint {
150    pub timezone: String,
151    pub locale: String,
152    pub latitude: Option<String>,
153    pub longitude: Option<String>,
154    pub accuracy_meters: Option<u32>,
155}
156
157/// Resolved WebRTC address fingerprint values.
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
159pub struct WebRtcFingerprint {
160    pub ipv4: Option<String>,
161    pub ipv6: Option<String>,
162}
163
164/// Resolved canvas fingerprint behavior.
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166pub struct CanvasFingerprint {
167    pub seed: PersonaSeed,
168    pub noise_enabled: bool,
169    pub noise_amplitude: u8,
170    pub blink_low_entropy_probe: bool,
171}
172
173/// Resolved DOMRect/SVGRect fingerprint behavior.
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
175pub struct DomRectFingerprint {
176    pub seed: PersonaSeed,
177    pub enabled: bool,
178    pub quantization_steps_per_px: u32,
179    pub fill_empty_client_rects: bool,
180}
181
182/// Resolved JavaScript engine fingerprint behavior.
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184pub struct EngineFingerprint {
185    pub enabled: bool,
186    pub to_fixed_range_error_message: String,
187    pub array_constructor_source: String,
188}
189
190/// Resolved audio fingerprint behavior.
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
192pub struct AudioFingerprint {
193    pub seed: PersonaSeed,
194    pub sample_rate: u32,
195    pub output_latency_ms: u32,
196    pub max_channel_count: u32,
197}
198
199/// Resolved font fingerprint values.
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
201pub struct FontFingerprint {
202    pub seed: PersonaSeed,
203    pub families: Vec<String>,
204}
205
206/// Resolved media devices and speech synthesis fingerprint values.
207#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
208pub struct MediaFingerprint {
209    pub speech_voices: Vec<String>,
210    pub audio_inputs: u32,
211    pub video_inputs: u32,
212    pub audio_outputs: u32,
213}
214
215/// Resolved battery fingerprint values.
216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
217pub struct BatteryFingerprint {
218    pub charging: bool,
219    pub charging_time_seconds: u32,
220    pub discharging_time_seconds: Option<u32>,
221    pub level_percent: u32,
222}
223
224/// Resolved storage quota fingerprint values.
225#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
226pub struct StorageFingerprint {
227    pub quota_bytes: Option<u64>,
228}
229
230/// Complete resolved persona consumed by the runtime and Servo preferences.
231#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
232pub struct ResolvedPersona {
233    pub seed: PersonaSeed,
234    pub preset: BrowserPersonaPreset,
235    pub startup_url: Option<String>,
236    pub browser_name: String,
237    pub browser_version: String,
238    pub platform_name: String,
239    pub viewport: Viewport,
240    pub screen: ScreenFingerprint,
241    pub window: WindowFingerprint,
242    pub css: CssFingerprint,
243    pub network: NetworkFingerprint,
244    pub graphics: GraphicsFingerprint,
245    pub js: JsFingerprint,
246    pub geo: GeoFingerprint,
247    pub webrtc: WebRtcFingerprint,
248    pub canvas: CanvasFingerprint,
249    pub domrect: DomRectFingerprint,
250    pub engine: EngineFingerprint,
251    pub audio: AudioFingerprint,
252    pub fonts: FontFingerprint,
253    pub media: MediaFingerprint,
254    pub battery: BatteryFingerprint,
255    pub storage: StorageFingerprint,
256    pub automation: AutomationPolicy,
257}
258
259pub type BrowserPersona = ResolvedPersona;
260
261impl ResolvedPersona {
262    pub fn from_preset(preset: BrowserPersonaPreset) -> Self {
263        Self::from_preset_and_seed(
264            preset,
265            PersonaSeed::from_stable_input(format!("preset:{preset:?}")),
266        )
267    }
268
269    pub fn from_preset_and_seed(preset: BrowserPersonaPreset, seed: PersonaSeed) -> Self {
270        match preset {
271            BrowserPersonaPreset::ChromeStable => Self::chrome_stable(seed),
272        }
273    }
274
275    pub fn chrome_stable(seed: PersonaSeed) -> Self {
276        let viewport = Viewport::default();
277        Self {
278            seed: seed.clone(),
279            preset: BrowserPersonaPreset::ChromeStable,
280            startup_url: None,
281            browser_name: "Chrome".to_string(),
282            browser_version: "136.0.7103.49".to_string(),
283            platform_name: "macOS".to_string(),
284            viewport: viewport.clone(),
285            screen: ScreenFingerprint::from_viewport(&viewport),
286            window: WindowFingerprint::from_viewport(&viewport),
287            css: CssFingerprint {
288                moz_prefix_enabled: false,
289                webkit_prefix_enabled: true,
290            },
291            network: NetworkFingerprint {
292                user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36".to_string(),
293                accept_language: "en-US,en;q=0.9".to_string(),
294                accept_header: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8".to_string(),
295                sec_ch_ua: "\"Chromium\";v=\"136\", \"Google Chrome\";v=\"136\", \"Not.A/Brand\";v=\"99\"".to_string(),
296                sec_ch_ua_mobile: "?0".to_string(),
297                sec_ch_ua_platform: "\"macOS\"".to_string(),
298            },
299            graphics: GraphicsFingerprint {
300                webgl_vendor: "Intel Inc.".to_string(),
301                webgl_renderer: "Intel(R) Iris(TM) Plus Graphics OpenGL Engine".to_string(),
302                webgl_masked_vendor: "WebKit".to_string(),
303                webgl_masked_renderer: "WebKit WebGL".to_string(),
304                webgl_seed: PersonaSeed::from_stable_input(format!("{seed}:webgl")),
305            },
306            js: JsFingerprint {
307                app_version: "5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36".to_string(),
308                platform: "MacIntel".to_string(),
309                language: "en-US".to_string(),
310                languages: vec!["en-US".to_string(), "en".to_string()],
311                hardware_concurrency: 8,
312                device_memory_gb: 8,
313                max_touch_points: 0,
314                do_not_track: String::new(),
315                expose_global_privacy_control: false,
316                global_privacy_control: false,
317                permissions_enabled: true,
318                bluetooth_enabled: true,
319                bluetooth_available: true,
320                media_devices_enabled: true,
321                webgpu_enabled: true,
322                offscreen_canvas_enabled: true,
323                service_worker_enabled: true,
324                ua_platform_version: "15.7.0".to_string(),
325                ua_architecture: "x86".to_string(),
326                ua_bitness: "64".to_string(),
327                ua_model: String::new(),
328                timezone: "America/Los_Angeles".to_string(),
329                vendor: "Google Inc.".to_string(),
330                product_sub: "20030107".to_string(),
331                pdf_viewer_enabled: true,
332            },
333            geo: GeoFingerprint {
334                timezone: "America/Los_Angeles".to_string(),
335                locale: "en-US".to_string(),
336                latitude: None,
337                longitude: None,
338                accuracy_meters: None,
339            },
340            webrtc: WebRtcFingerprint {
341                ipv4: None,
342                ipv6: None,
343            },
344            canvas: CanvasFingerprint {
345                seed: PersonaSeed::from_stable_input(format!("{seed}:canvas")),
346                noise_enabled: true,
347                noise_amplitude: 1,
348                blink_low_entropy_probe: true,
349            },
350            domrect: DomRectFingerprint {
351                seed: PersonaSeed::from_stable_input(format!("{seed}:domrect")),
352                enabled: true,
353                quantization_steps_per_px: 64,
354                fill_empty_client_rects: true,
355            },
356            engine: EngineFingerprint {
357                enabled: true,
358                to_fixed_range_error_message: "toFixed() digits argument must be between 0-100"
359                    .to_string(),
360                array_constructor_source: "function Array() { [native code] }".to_string(),
361            },
362            audio: AudioFingerprint {
363                seed: PersonaSeed::from_stable_input(format!("{seed}:audio")),
364                sample_rate: 48_000,
365                output_latency_ms: 20,
366                max_channel_count: 2,
367            },
368            fonts: FontFingerprint {
369                seed: PersonaSeed::from_stable_input(format!("{seed}:fonts")),
370                families: vec![
371                    "Arial".to_string(),
372                    "Helvetica".to_string(),
373                    "Times New Roman".to_string(),
374                    "Courier New".to_string(),
375                ],
376            },
377            media: MediaFingerprint {
378                speech_voices: vec!["Samantha".to_string(), "Alex".to_string()],
379                audio_inputs: 1,
380                video_inputs: 1,
381                audio_outputs: 1,
382            },
383            battery: BatteryFingerprint {
384                charging: true,
385                charging_time_seconds: 0,
386                discharging_time_seconds: None,
387                level_percent: 100,
388            },
389            storage: StorageFingerprint { quota_bytes: None },
390            automation: AutomationPolicy::default(),
391        }
392    }
393}