diff --git a/.github/workflows/anti-fingerprint.yml b/.github/workflows/anti-fingerprint.yml index 68a4906..01cded2 100644 --- a/.github/workflows/anti-fingerprint.yml +++ b/.github/workflows/anti-fingerprint.yml @@ -98,13 +98,14 @@ jobs: run: | cd lw echo "$TOR_ESR_VERSION" > version - # Tor mode omits the bearbrowser anti-fp patches (CanvasTextMetrics + - # WebAudioFarble) — they are DISABLED to match the Tor Browser cohort, so - # compiling them in is pointless, and these 150-authored patches reject on - # the 140 ESR tree anyway (the default 150 build keeps them). So the - # Tor-mode ESR stack is the LibreWolf stack only, matching what - # apply-sourceos-overlays.sh --profile tor-mode injects (i.e. nothing). - echo "tor-mode stack: $(grep -c . assets/patches.txt) LibreWolf patches against firefox-$(cat version) (anti-fp patches omitted by design)" + # Tor mode omits the canvas/audio anti-fp patches (disabled to match the + # cohort; they're 150-authored and reject on 140 anyway) — but it DOES + # need the OS-spoof patch: forcing the Windows identity is a cohort + # requirement. Register only that one, matching apply-sourceos-overlays.sh + # --profile tor-mode. + cp ../overlay/gecko-patches/anti-fingerprint/anti-fp-tor-os-spoof.patch patches/ + echo "patches/anti-fp-tor-os-spoof.patch" >> assets/patches.txt + echo "tor-mode stack: $(grep -c . assets/patches.txt) patches (LibreWolf + OS-spoof) against firefox-$(cat version)" - name: Fetch ESR source run: | cd lw diff --git a/docs/tor-mode.md b/docs/tor-mode.md index 94cec9d..be0f2fd 100644 --- a/docs/tor-mode.md +++ b/docs/tor-mode.md @@ -42,17 +42,28 @@ value.** Three cases: > You cannot be *uniquely-best-BearBrowser* and *network-anonymous* at the same > time. You pick per session. That's the honest physics of it. -## §OS spoof — the biggest lever (needs a patch, not a pref) +## §OS spoof — the biggest lever (a compile-time patch) Tor Browser presents `Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) -Gecko/20100101 Firefox/140.0` for **all** desktop platforms. RFP computes its own -UA and **ignores** `general.useragent.override`, so this cannot be a pref — it is an -`nsRFPService` patch across ~8 use-sites (UA, platform, oscpu, appVersion, worker -equivalents, maxTouchPoints). Fully specified in -`gecko-patches/anti-fingerprint/anti-fp-tor-os-spoof.SPEC.md`; the `tor-mode` -profile already sets the trigger pref `bearbrowser.tor-mode.spoof-os=windows` -(a no-op until the patch lands). It is a *coordinated* change — one missed site = -an inconsistent identity that is worse than no spoof — so it is authored with a -compiler in the loop, not blind. +Gecko/20100101 Firefox/140.0` for **all** desktop platforms, so Mac/Linux users +hide in the Windows majority. RFP computes its own UA and **ignores** +`general.useragent.override`, so this must be a patch. + +The patch (`gecko-patches/anti-fingerprint/anti-fp-tor-os-spoof.patch`, **applies +cleanly to firefox-140.12.0** — verified locally + in CI) turned out far smaller +than first specced. Every spoofed value except `navigator.platform` flows from the +`SPOOFED_*` macros in `nsRFPService.h` — `navigator.userAgent` (nsRFPService), +`navigator.oscpu`/`appVersion` (Navigator + WorkerNavigator), and the worker copies +(RuntimeService calls `Navigator::Get*`). So **one `#ifdef` in the header** forces +UA + oscpu + appVersion consistently, and `navigator.platform` (the one hardcoded +site) gets a one-line condition. No per-site coordination, no runtime pref, no +StaticPrefs codegen — so no inconsistent-identity risk. + +It is gated on `-DBEARBROWSER_FORCE_WIN_SPOOF`, which `apply-sourceos-overlays.sh +--profile tor-mode` sets in the workspace mozconfig and injects the patch. The +default 150 build never defines it → real OS. `maxTouchPoints` stays 0 (Tor desktop) +rather than Windows' 10. Compile-verification lands with the Forgejo build; the +SPEC file (`anti-fp-tor-os-spoof.SPEC.md`) documented the original ~8-site runtime +approach and is now superseded by this simpler compile-time patch. ## §version — ride the 140 line, not 150 Default BearBrowser is **Firefox 150**; the live Tor cohort is **Firefox 140 ESR**. diff --git a/gecko-patches/anti-fingerprint/anti-fp-tor-os-spoof.patch b/gecko-patches/anti-fingerprint/anti-fp-tor-os-spoof.patch new file mode 100644 index 0000000..c9848a9 --- /dev/null +++ b/gecko-patches/anti-fingerprint/anti-fp-tor-os-spoof.patch @@ -0,0 +1,52 @@ +BearBrowser Tor mode — force the Windows identity on all desktop platforms. + +Tor Browser presents `Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) +Gecko/20100101 Firefox/140.0` for EVERY desktop platform, so Mac/Linux users hide +in the Windows majority. Stock RFP keeps the real OS family, which would split our +Mac/Linux Tor-mode users into a tiny distinct cohort. + +This is done at COMPILE time, gated on -DBEARBROWSER_FORCE_WIN_SPOOF, which the +tor-mode build sets in its mozconfig (apply-sourceos-overlays.sh --profile +tor-mode). The default 150 build does NOT define it, so it reports the real OS. + +Because navigator.userAgent (nsRFPService), navigator.oscpu / navigator.appVersion +(Navigator + WorkerNavigator), and the worker copies (RuntimeService calls +Navigator::Get*) all flow from the SPOOFED_* macros in nsRFPService.h, one #ifdef +in the header forces UA + oscpu + appVersion consistently. navigator.platform is +the only value not macro-driven (hardcoded per-platform in Navigator::GetPlatform), +so it gets a one-line condition. MAX_TOUCH_POINTS stays 0 — Tor desktop reports 0, +not Windows' 10. + +diff --git a/toolkit/components/resistfingerprinting/nsRFPService.h b/toolkit/components/resistfingerprinting/nsRFPService.h +--- a/toolkit/components/resistfingerprinting/nsRFPService.h ++++ b/toolkit/components/resistfingerprinting/nsRFPService.h +@@ -38,7 +38,16 @@ + // the platform-specific definitions. + #define SPOOFED_UA_OS_OTHER "X11; Linux x86_64" + +-#ifdef XP_WIN ++// BearBrowser Tor mode: present the Windows identity on ALL desktop platforms so ++// Mac/Linux users hide in the Windows majority (the Tor Browser cohort). Enabled ++// at compile time via -DBEARBROWSER_FORCE_WIN_SPOOF (tor-mode build only). ++// MAX_TOUCH_POINTS stays 0 — Tor desktop reports 0, not Windows' 10. ++#ifdef BEARBROWSER_FORCE_WIN_SPOOF ++# define SPOOFED_UA_OS "Windows NT 10.0; Win64; x64" ++# define SPOOFED_APPVERSION "5.0 (Windows)" ++# define SPOOFED_OSCPU "Windows NT 10.0; Win64; x64" ++# define SPOOFED_MAX_TOUCH_POINTS 0 ++#elif defined(XP_WIN) + # define SPOOFED_UA_OS "Windows NT 10.0; Win64; x64" + # define SPOOFED_APPVERSION "5.0 (Windows)" + # define SPOOFED_OSCPU "Windows NT 10.0; Win64; x64" +diff --git a/dom/base/Navigator.cpp b/dom/base/Navigator.cpp +--- a/dom/base/Navigator.cpp ++++ b/dom/base/Navigator.cpp +@@ -2005,7 +2005,7 @@ + } + } + +-#if defined(WIN32) ++#if defined(WIN32) || defined(BEARBROWSER_FORCE_WIN_SPOOF) + aPlatform.AssignLiteral("Win32"); + #elif defined(XP_MACOSX) + // Always return "MacIntel", even on ARM64 macOS like Safari does. diff --git a/scripts/apply-sourceos-overlays.sh b/scripts/apply-sourceos-overlays.sh index 4884d7e..dd44bb1 100755 --- a/scripts/apply-sourceos-overlays.sh +++ b/scripts/apply-sourceos-overlays.sh @@ -248,7 +248,29 @@ done afp_dir="$repo_root/gecko-patches/anti-fingerprint" patches_txt="$workspace/source/assets/patches.txt" if [ "$profile" = "tor-mode" ]; then - echo "feature-layer: tor-mode omits bearbrowser anti-fp patches (disabled to match cohort; see docs/tor-mode.md)" + echo "feature-layer: tor-mode omits the canvas/audio anti-fp patches (disabled to match cohort; see docs/tor-mode.md)" + # ...but tor-mode DOES need the OS-spoof patch: Tor forces the Windows identity + # on all desktop platforms, which is a cohort REQUIREMENT (not one of our extra + # protections). It's gated on -DBEARBROWSER_FORCE_WIN_SPOOF, set below. + osspoof="anti-fp-tor-os-spoof.patch" + if [ -f "$afp_dir/$osspoof" ] && [ -f "$patches_txt" ]; then + mkdir -p "$workspace/source/patches" + cp "$afp_dir/$osspoof" "$workspace/source/patches/$osspoof" + grep -qxF "patches/$osspoof" "$patches_txt" || echo "patches/$osspoof" >> "$patches_txt" + echo "feature-layer: registered tor-mode OS-spoof patch $osspoof" + fi + # Turn the spoof on at compile time. The macro gates nsRFPService.h + Navigator + # .cpp so the whole identity (UA/oscpu/appVersion/platform, main + workers) + # reports Windows. The default 150 build never sets it -> real OS. + mozcfg="$workspace/source/assets/mozconfig" + if [ -f "$mozcfg" ] && ! grep -q "BEARBROWSER_FORCE_WIN_SPOOF" "$mozcfg"; then + { + echo '' + echo '# BearBrowser Tor mode: present the Windows identity (Tor Browser cohort).' + echo 'export CXXFLAGS="${CXXFLAGS} -DBEARBROWSER_FORCE_WIN_SPOOF"' + } >> "$mozcfg" + echo "feature-layer: tor-mode mozconfig sets -DBEARBROWSER_FORCE_WIN_SPOOF" + fi elif [ -d "$afp_dir" ] && [ -f "$patches_txt" ]; then mkdir -p "$workspace/source/patches" for p in anti-fp-canvas-text-metrics.patch anti-fp-audio.patch; do diff --git a/settings/profiles/tor-mode/user.js b/settings/profiles/tor-mode/user.js index 5587325..8167d59 100644 --- a/settings/profiles/tor-mode/user.js +++ b/settings/profiles/tor-mode/user.js @@ -88,11 +88,10 @@ user_pref("webgl.enable-debug-renderer-info", false); // mask real GPU vendor/r // // (c) The BIG one: Tor makes EVERY desktop platform report Windows so Mac/Linux // users hide in the Windows majority. RFP computes its own UA and IGNORES -// general.useragent/platform/oscpu.override, so this CANNOT be a pref — it needs -// the nsRFPService OS-spoof patch. This pref is the trigger that patch reads; it is -// a NO-OP until the patch lands. See -// gecko-patches/anti-fingerprint/anti-fp-tor-os-spoof.SPEC.md. -user_pref("bearbrowser.tor-mode.spoof-os", "windows"); +// general.useragent/platform/oscpu.override, so this is NOT a pref — it is the +// compile-time OS-spoof patch (gecko-patches/anti-fingerprint/ +// anti-fp-tor-os-spoof.patch), built in via -DBEARBROWSER_FORCE_WIN_SPOOF which +// apply-sourceos-overlays.sh --profile tor-mode sets. Nothing to set here. // // Match Tor Browser's first-party isolation posture. user_pref("privacy.firstparty.isolate", false); // dFPI supersedes; keep consistent