Files
sovereign_browser_rust/plans/browser-fingerprint-spoofing.md
Laan Tungir f4250de54c v0.0.16 - Add browser fingerprint spoofing (desktop + mobile identities)
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).
2026-10-06 16:16:50 -04:00

274 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```mermaid
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`:
```rust
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
1. **UA string (engine level):** call `Settings::set_user_agent(Some(ua))` on
each tab's `WebKitSettings` in `tab_manager_new_tab()`.
2. **JS surface (main world, document-start):** new
`www/js/fingerprint-spoof.js` embedded via `include_str!`, plus a small
preamble that sets `window.__SB_FP = { ...profile json... }`. Added to the
tab's `UserContentManager` before the webview is built. A preamble guard
skips `sovereign:` pages (same pattern as the perf probe) so internal pages
are unaffected.
3. **Overrides applied by the script:**
- `navigator.userAgent`, `appVersion`, `platform`, `vendor`, `productSub`,
`oscpu`, `hardwareConcurrency`, `deviceMemory`, `maxTouchPoints`,
`language`, `languages`
- `navigator.userAgentData` (brands, mobile, platform, and
`getHighEntropyValues`)
- `screen.width/height/availWidth/availHeight/colorDepth/pixelDepth`,
`window.devicePixelRatio`
- `WebGLRenderingContext.prototype.getParameter` and
`WebGL2RenderingContext.prototype.getParameter` for
`UNMASKED_VENDOR_WEBGL` (37445) and `UNMASKED_RENDERER_WEBGL` (37446)
- `'ontouchstart' in window` and `navigator.maxTouchPoints` for mobile
- `Intl.DateTimeFormat().resolvedOptions().timeZone` (optional)
4. **Optional request headers:** `WebView::connect_send_request` to add
`Sec-CH-UA`, `Sec-CH-UA-Mobile`, `Sec-CH-UA-Platform` so HTTP-level Client
Hints match the JS surface.
## Settings & Persistence
Add to `BrowserSettings` (`src/settings.rs`):
- `fingerprint_spoof_enabled: bool` (default `false`)
- `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`: add `fingerprint_spoof_enabled`,
`user_agent_mode`, `user_agent_preset`, `user_agent_custom`, and a
`user_agent_presets` array (`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()`).
## 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.rs` `build_hamburger_menu`).
- Internal debug page `sovereign://fingerprint` showing the active spoofed
values for verification.
## Deferred
- **Plan item 9 — `Sec-CH-UA*` request headers (deferred).** Sending
`Sec-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_request`
only 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** (a `WebKitWebProcessExtension` loaded into the WebProcess) that
intercepts requests there. Until then the HTTP-level Client Hints do not
match the JS `navigator.userAgentData` surface, which is itself a
fingerprinting signal — documented as a known limitation in
`www/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.userAgent` matches the `Sec-CH-UA` headers 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 config
- `src/lib.rs`, `src/main.rs` — register module
- `src/settings.rs` — new fields, defaults, update/save/load
- `src/settings_sync.rs` — include new fields in payload
- `src/web_context.rs` — apply UA string in settings configuration
- `src/nostr_inject.rs` — add fingerprint script to content manager
- `www/js/fingerprint-spoof.js` (new) — JS overrides
- `src/tab_manager.rs` — wire UA + injection into tab creation; reload-all helper
- `src/nostr_bridge.rs` — config JSON + set handlers
- `www/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`, `outerWidth` and
`outerHeight` are now installed as own accessor properties of the `window`
instance (via a new `defineWindowGetter` helper) instead of on
`Window.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-internet` and
`firefox-android` now set `ua_architecture = "arm"` (Android devices report
"arm"). `deviceMemory` and the Firefox `buildID` were left unchanged.
### Bridge (`src/nostr_bridge.rs`)
- `handle_settings_config_json`: added `fingerprint_spoof_enabled`,
`user_agent_mode`, `user_agent_preset`, `user_agent_custom`,
`user_agent_presets` (`presets_json()`) and `active_fingerprint`
(`serde_json::to_value(resolve(&s))`, or `null`).
- `handle_settings_set`:
- feature `fingerprint_spoof_enabled` — flips and saves; when turning ON
while `user_agent_mode == "default"` it also sets the mode to `"preset"`.
- key `user_agent_mode` — validates `default|preset|custom` (else 400).
- key `user_agent_preset` — validates via `preset_by_id` (else 400
"Unknown preset"); also sets `user_agent_mode = "preset"`.
- key `user_agent_custom` — trims; also sets `user_agent_mode = "custom"`
when non-empty.
- All of the above schedule `tab_manager_apply_fingerprint_all()` with
`glib::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 no `reload` flag.
- New `handle_fingerprint_data_json` for `sovereign://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`/`Bots` optgroups + `Custom…`), custom-UA input row
(shown only for `Custom…`), read-only "Active User-Agent" line, and a
"View details" link to `sovereign://fingerprint`.
- `applyConfig` adds `fingerprint_spoof_enabled` to `toggleMap` and calls the
new `renderIdentity(d)`; `saveIdentity()` / `saveCustomUA()` call the bridge
and then `refreshConfig()` (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_menu` adds a "Browser Identity" submenu in the Network
group. Contents are rebuilt lazily on `connect_show` (like Recents/Bookmarks)
so the checked state is always current. Radio items are all in one group;
the active state is set before `toggled` is connected, so building the menu
never fires an action. Helpers: `identity_add_radio` and `identity_apply`
(persists settings, then
`glib::idle_add_local_once(tab_manager_apply_fingerprint_all)`).
### Debug page `sovereign://fingerprint`
- New `www/fingerprint.html`, `www/fingerprint.css`, `www/fingerprint.js`,
registered in `src/embedded_web_content.rs` (`get_fingerprint_html`,
`WWW_FINGERPRINT_CSS`, `WWW_FINGERPRINT_JS`, and the `get_content_with_mime`
match arms). The `fingerprint/data` route is dispatched in
`handle_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`, and
`node --check` on a temp copy of `www/js/fingerprint-spoof.js` (with `({});`
appended) — all pass.