Present the browser as Chrome, Firefox, Safari, Edge, or a mobile browser (Chrome Android, Safari iOS, Samsung Internet, Firefox Android) plus Googlebot/Bingbot, at both the User-Agent and JavaScript level. - src/fingerprint.rs: 15 internally-consistent presets, resolve(), JS config - www/js/fingerprint-spoof.js: stealthy overrides for navigator.*, userAgentData (Client Hints), screen, devicePixelRatio, outer size, touch points, matchMedia, WebGL vendor/renderer, window.chrome - Engine UA via Settings::set_user_agent with site-specific quirks disabled - Settings UI (Browser Identity section), hamburger submenu, and a sovereign://fingerprint debug page comparing spoofed vs real values - Settings persist and sync via NIP-78 Verified in real WebKitGTK 2.52.6 across six presets; sovereign:// pages are never spoofed. Sec-CH-UA request headers are deferred (needs a WebKitGTK web-process extension).
12 KiB
Browser Fingerprint Spoofing (Desktop + Mobile)
Goal
Let sovereign_browser present itself as a different browser — Chrome, Firefox, Safari, Edge, and mobile browsers (Chrome Android, Safari iOS, Samsung Internet, Firefox Android) — at both the User-Agent string level and the JavaScript fingerprint level, so sites and scripts see a consistent impersonated browser.
Scope
- In scope: UA string,
navigator.*overrides,navigator.userAgentData(Client Hints JS API),screen.*,devicePixelRatio, WebGL vendor/renderer, touch points, languages, and (optionally)Sec-CH-UA*request headers. - Out of scope (documented as caveats): TLS/JA3/JA4 fingerprints, HTTP/2 frame fingerprints, and true engine-level differences. WebKitGTK remains the engine; this is UA + JS-surface spoofing only.
Architecture
flowchart TD
A[Settings UI sovereign://settings] --> B[Bridge settings/set]
B --> C[BrowserSettings store]
C --> D[fingerprint module resolve]
D --> E[FingerprintProfile]
E --> F[WebKit Settings set_user_agent]
E --> G[UserContentManager injected JS]
E --> H[Optional Sec-CH-UA request headers]
F --> I[WebView]
G --> I
H --> I
Data Model
New module src/fingerprint.rs:
pub struct FingerprintProfile {
pub id: String,
pub label: String,
pub category: Category, // Desktop | Mobile | Bot | Custom
pub user_agent: String,
pub platform: String, // navigator.platform
pub vendor: String, // navigator.vendor
pub app_version: String, // navigator.appVersion
pub oscpu: Option<String>, // navigator.oscpu (Firefox)
pub product_sub: String, // navigator.productSub
pub ua_brands: Vec<(String, String)>, // userAgentData.brands
pub ua_mobile: bool, // userAgentData.mobile
pub ua_platform: String, // userAgentData.platform
pub hardware_concurrency: u32,
pub device_memory: u32,
pub max_touch_points: u32, // 0 desktop, 5 mobile
pub screen: ScreenInfo, // width/height/avail/colorDepth/pixelDepth
pub device_pixel_ratio: f64,
pub languages: Vec<String>,
pub webgl_vendor: String,
pub webgl_renderer: String,
}
resolve(settings) -> Option<FingerprintProfile> returns the active profile
from user_agent_mode (default | preset | custom), or None when
spoofing is off.
Preset Catalog
Built-in presets, grouped by category:
- Desktop: Chrome (Windows/macOS/Linux), Firefox (Windows/macOS/Linux), Safari (macOS), Edge (Windows).
- Mobile: Chrome Android, Safari iOS (iPhone), Safari iOS (iPad), Samsung Internet (Android), Firefox Android.
- Bots: Googlebot, Bingbot.
Each preset carries a fully consistent set of values (UA string, platform, vendor, brands, screen size, DPR, touch points, WebGL strings) so the spoofed surface does not contradict itself.
Injection Strategy
- UA string (engine level): call
Settings::set_user_agent(Some(ua))on each tab'sWebKitSettingsintab_manager_new_tab(). - JS surface (main world, document-start): new
www/js/fingerprint-spoof.jsembedded viainclude_str!, plus a small preamble that setswindow.__SB_FP = { ...profile json... }. Added to the tab'sUserContentManagerbefore the webview is built. A preamble guard skipssovereign:pages (same pattern as the perf probe) so internal pages are unaffected. - Overrides applied by the script:
navigator.userAgent,appVersion,platform,vendor,productSub,oscpu,hardwareConcurrency,deviceMemory,maxTouchPoints,language,languagesnavigator.userAgentData(brands, mobile, platform, andgetHighEntropyValues)screen.width/height/availWidth/availHeight/colorDepth/pixelDepth,window.devicePixelRatioWebGLRenderingContext.prototype.getParameterandWebGL2RenderingContext.prototype.getParameterforUNMASKED_VENDOR_WEBGL(37445) andUNMASKED_RENDERER_WEBGL(37446)'ontouchstart' in windowandnavigator.maxTouchPointsfor mobileIntl.DateTimeFormat().resolvedOptions().timeZone(optional)
- Optional request headers:
WebView::connect_send_requestto addSec-CH-UA,Sec-CH-UA-Mobile,Sec-CH-UA-Platformso HTTP-level Client Hints match the JS surface.
Settings & Persistence
Add to BrowserSettings (src/settings.rs):
fingerprint_spoof_enabled: bool(defaultfalse)user_agent_mode: String(default"default")user_agent_preset: String(default"")user_agent_custom: String(default"")
Update Default, settings_update, settings_save, and settings_load.
Include the fields in the NIP-78 payload in src/settings_sync.rs.
Bridge API (src/nostr_bridge.rs)
handle_settings_config_json: addfingerprint_spoof_enabled,user_agent_mode,user_agent_preset,user_agent_custom, and auser_agent_presetsarray (id,label,category,ua) for the UI.handle_settings_set:- feature toggle
fingerprint_spoof_enabled - keys
user_agent_mode,user_agent_preset,user_agent_custom - on change, reload open tabs so the new identity takes effect
(add
tab_manager_reload_all_tabs()).
- feature toggle
Settings UI (www/settings.html, www/settings.js)
New Browser Identity section:
- Master toggle: "Spoof browser fingerprint"
- Preset dropdown, grouped by Desktop / Mobile / Bots / Custom
- Custom UA text input (enabled when Custom is selected)
- Link to a fingerprint test page (e.g. browserleaks.com)
applyConfig populates the controls; toggle/save call the bridge.
Optional Enhancements
- Hamburger menu submenu "Browser Identity" with radio items for quick preset
switching (
src/tab_manager.rsbuild_hamburger_menu). - Internal debug page
sovereign://fingerprintshowing the active spoofed values for verification.
Deferred
- Plan item 9 —
Sec-CH-UA*request headers (deferred). SendingSec-CH-UA,Sec-CH-UA-Mobile,Sec-CH-UA-Platform(and the high-entropy variants) on subresource requests is not implemented. WebKitGTK's UI process cannot modify subresource request headers:WebView::connect_send_requestonly fires for the main resource, and there is no UI-process API to rewrite headers for subresources. Doing this correctly requires a web-process extension (aWebKitWebProcessExtensionloaded into the WebProcess) that intercepts requests there. Until then the HTTP-level Client Hints do not match the JSnavigator.userAgentDatasurface, which is itself a fingerprinting signal — documented as a known limitation inwww/js/fingerprint-spoof.js.
Caveats
- Engine-level fingerprints (TLS, HTTP/2, JS engine quirks) still identify WebKitGTK; this feature spoofs the UA and JS-visible surface only.
- All spoofed values must stay internally consistent; inconsistent presets are themselves a fingerprinting signal.
- Mobile spoofing changes JS-visible values, not the actual rendering viewport.
Testing
- Verify against browserleaks.com, whatismybrowser.com, and a Client Hints test page for each preset (desktop + mobile).
- Confirm
navigator.userAgentmatches theSec-CH-UAheaders and the engine-level UA. - Confirm internal
sovereign://pages are unaffected. - Confirm settings persist across restart and sync via NIP-78.
File-by-File Change List
src/fingerprint.rs(new) — profile model, preset catalog, resolve, JS configsrc/lib.rs,src/main.rs— register modulesrc/settings.rs— new fields, defaults, update/save/loadsrc/settings_sync.rs— include new fields in payloadsrc/web_context.rs— apply UA string in settings configurationsrc/nostr_inject.rs— add fingerprint script to content managerwww/js/fingerprint-spoof.js(new) — JS overridessrc/tab_manager.rs— wire UA + injection into tab creation; reload-all helpersrc/nostr_bridge.rs— config JSON + set handlerswww/settings.html,www/settings.js— Browser Identity UI
Implementation status
Phases 1–2 (data layer, UA application, JS injection) are done and reviewed. Phase 3 (settings UI, bridge wiring, hamburger submenu, debug page) is done.
Phase 3 fixes from review
www/js/fingerprint-spoof.js:devicePixelRatio,outerWidthandouterHeightare now installed as own accessor properties of thewindowinstance (via a newdefineWindowGetterhelper) instead of onWindow.prototype. In WebKitGTK these are own accessors of the window instance, so prototype getters were shadowed and had no effect. The getters are still passed through the native-toString masking helper.src/fingerprint.rs:chrome-android,samsung-internetandfirefox-androidnow setua_architecture = "arm"(Android devices report "arm").deviceMemoryand the FirefoxbuildIDwere left unchanged.
Bridge (src/nostr_bridge.rs)
handle_settings_config_json: addedfingerprint_spoof_enabled,user_agent_mode,user_agent_preset,user_agent_custom,user_agent_presets(presets_json()) andactive_fingerprint(serde_json::to_value(resolve(&s)), ornull).handle_settings_set:- feature
fingerprint_spoof_enabled— flips and saves; when turning ON whileuser_agent_mode == "default"it also sets the mode to"preset". - key
user_agent_mode— validatesdefault|preset|custom(else 400). - key
user_agent_preset— validates viapreset_by_id(else 400 "Unknown preset"); also setsuser_agent_mode = "preset". - key
user_agent_custom— trims; also setsuser_agent_mode = "custom"when non-empty. - All of the above schedule
tab_manager_apply_fingerprint_all()withglib::idle_add_local_once(...)(never inline — we are inside a URI-scheme request callback and must not reload webviews mid-request). Response is{key, value}with noreloadflag.
- feature
- New
handle_fingerprint_data_jsonforsovereign://fingerprint/data.
Settings UI (www/settings.html, www/settings.js)
- New "Browser Identity" section (after Search, before Network): master toggle
(
data-feature="fingerprint_spoof_enabled"), grouped preset<select>(Desktop/Mobile/Botsoptgroups +Custom…), custom-UA input row (shown only forCustom…), read-only "Active User-Agent" line, and a "View details" link tosovereign://fingerprint. applyConfigaddsfingerprint_spoof_enabledtotoggleMapand calls the newrenderIdentity(d);saveIdentity()/saveCustomUA()call the bridge and thenrefreshConfig()(re-fetch +applyConfig, no full reload);updateCustomRowVisibility()toggles the custom row; controls are disabled (but visible) when spoofing is off.
Hamburger submenu (src/tab_manager.rs)
build_hamburger_menuadds a "Browser Identity" submenu in the Network group. Contents are rebuilt lazily onconnect_show(like Recents/Bookmarks) so the checked state is always current. Radio items are all in one group; the active state is set beforetoggledis connected, so building the menu never fires an action. Helpers:identity_add_radioandidentity_apply(persists settings, thenglib::idle_add_local_once(tab_manager_apply_fingerprint_all)).
Debug page sovereign://fingerprint
- New
www/fingerprint.html,www/fingerprint.css,www/fingerprint.js, registered insrc/embedded_web_content.rs(get_fingerprint_html,WWW_FINGERPRINT_CSS,WWW_FINGERPRINT_JS, and theget_content_with_mimematch arms). Thefingerprint/dataroute is dispatched inhandle_sovereign_scheme(src/nostr_bridge.rs). - The page shows status, a spoofed-vs-real comparison table (the real column is
read live from the unspoofed
sovereign://page), known limitations, and links to browserleaks / whatismybrowser / creepjs.
Verification
cargo build— clean, no warnings.cargo test— 38 passed, 0 failed.node --check www/settings.js,node --check www/fingerprint.js, andnode --checkon a temp copy ofwww/js/fingerprint-spoof.js(with({});appended) — all pass.