Cookie system: - Expand from 5 to 12 fully-specified CookiePolicy modes in browser_identity.py - Add CookiePolicy dataclass: behavior, lifetime, TCP partitioning, clearOnShutdown.* - browser_profile.py emits full cookie pref set from policy object - UI dropdown widened to 680px with live description label per mode Firefox launch fix: - Detect Firefox via Windows registry, AppData, and shutil.which - Use DETACHED_PROCESS|CREATE_NO_WINDOW|CREATE_NEW_PROCESS_GROUP flags - Wait up to 3s for firefox.exe in tasklist instead of polling parent pid - taskkill on stop() to terminate all firefox.exe processes DNS leak tester: - dns_leak.py: FullDnsLeakReport dataclass, run_dns_leak_test comparing proxy DoH resolution vs direct system DNS WebRTC tester: - webrtc_check.py: STUN UDP probe, registry policy check, user.js pref check Ban tester: - Added SITES_SHOPPING, SITES_CRYPTO, SITES_DNS categories - Parallel execution via ThreadPoolExecutor - Expanded banned-text hint keywords Fingerprint audit: - OS identity checks: hostname, MAC, GUID, OS version, timezone, screen res - Browser consistency analysis of user.js Neon world map: - world_map.png bundled; chain_map.py renders hop arcs over it with glow effect Signup prep: - Auto-save account on Open & Autofill; Copy Email / Copy Pass buttons - Auto-fill custom URL when preset site selected - PyInstaller-safe path resolution for signup_extension Spec: - Bundle signup_extension dir and world_map.png as PyInstaller data files Co-authored-by: Cursor <cursoragent@cursor.com>
381 lines
14 KiB
Python
381 lines
14 KiB
Python
"""Device fingerprint audit and OS-level hardening helpers."""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import random
|
|
import re
|
|
import string
|
|
import subprocess
|
|
import winreg
|
|
from dataclasses import dataclass, field
|
|
|
|
from .firewall import is_admin
|
|
from .mac_spoof import list_nics
|
|
from .win_compat import probe as _win_probe
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
_WEBRTC_CHROME = r"SOFTWARE\Policies\Google\Chrome"
|
|
_WEBRTC_EDGE = r"SOFTWARE\Policies\Microsoft\Edge"
|
|
_WEBRTC_VALUE = "DefaultWebRtcIpHandlingPolicy"
|
|
_WEBRTC_DISABLE = 2 # disable_non_proxied_udp
|
|
|
|
|
|
@dataclass
|
|
class FingerprintAudit:
|
|
lines: list[str] = field(default_factory=list)
|
|
hostname: str = ""
|
|
machine_guid: str = ""
|
|
username: str = ""
|
|
macs: list[str] = field(default_factory=list)
|
|
|
|
|
|
def _run_ps(script: str, timeout: float = 15.0) -> str:
|
|
try:
|
|
r = subprocess.run(
|
|
["powershell", "-NoProfile", "-Command", script],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout,
|
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
)
|
|
return (r.stdout or "").strip()
|
|
except Exception as e:
|
|
log.debug("fingerprint ps: %s", e)
|
|
return ""
|
|
|
|
|
|
def get_computer_name() -> str:
|
|
try:
|
|
import os
|
|
return os.environ.get("COMPUTERNAME", "") or ""
|
|
except Exception:
|
|
return ""
|
|
|
|
|
|
def get_machine_guid() -> str:
|
|
try:
|
|
with winreg.OpenKey(
|
|
winreg.HKEY_LOCAL_MACHINE,
|
|
r"SOFTWARE\Microsoft\Cryptography",
|
|
) as key:
|
|
val, _ = winreg.QueryValueEx(key, "MachineGuid")
|
|
return str(val)
|
|
except OSError:
|
|
return ""
|
|
|
|
|
|
def random_hostname(prefix: str = "PC") -> str:
|
|
suffix = "".join(random.choices(string.ascii_uppercase + string.digits, k=7))
|
|
return f"{prefix}-{suffix}"[:15]
|
|
|
|
|
|
def set_computer_name(name: str) -> tuple[bool, str]:
|
|
"""Set NetBIOS / computer name (Admin). Reboot may be required for all apps."""
|
|
if not is_admin():
|
|
return False, "Administrator required to change computer name."
|
|
name = re.sub(r"[^A-Za-z0-9\-]", "", name)[:15]
|
|
if len(name) < 1:
|
|
return False, "Invalid hostname."
|
|
code = subprocess.run(
|
|
["powershell", "-NoProfile", "-Command",
|
|
f'Rename-Computer -NewName "{name}" -Force -ErrorAction Stop'],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
).returncode
|
|
if code != 0:
|
|
# NetBIOS name via WMI (often works without full rename)
|
|
wmi = (
|
|
f'$n = Get-WmiObject Win32_ComputerSystem; '
|
|
f'$r = $n.Rename("{name}"); if ($r.ReturnValue -ne 0) {{ exit $r.ReturnValue }}'
|
|
)
|
|
code = subprocess.run(
|
|
["powershell", "-NoProfile", "-Command", wmi],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
).returncode
|
|
if code == 0:
|
|
return True, f"Computer name set to {name} (some apps need reconnect/reboot)."
|
|
return False, "Could not change computer name."
|
|
|
|
|
|
def disable_ipv6_on_adapters() -> tuple[list[str], list[str]]:
|
|
"""Disable IPv6 binding on up physical adapters. Returns (adapter names, log lines)."""
|
|
if not is_admin():
|
|
return [], ["IPv6 disable skipped (not Admin)."]
|
|
if not _win_probe().has_net_cmdlets:
|
|
return [], [
|
|
"IPv6 disable skipped — requires PowerShell 3.0+ "
|
|
"(Windows 8 / Server 2012+). Detected legacy PowerShell."
|
|
]
|
|
script = (
|
|
"Get-NetAdapter | Where-Object { $_.Status -eq 'Up' } | "
|
|
"ForEach-Object { $_.Name }"
|
|
)
|
|
names = [n.strip() for n in _run_ps(script).splitlines() if n.strip()]
|
|
logs: list[str] = []
|
|
changed: list[str] = []
|
|
skip = ("virtual", "vmware", "hyper-v", "loopback", "bluetooth")
|
|
for name in names:
|
|
if any(h in name.lower() for h in skip):
|
|
continue
|
|
cmd = (
|
|
f'Disable-NetAdapterBinding -Name "{name}" -ComponentID ms_tcpip6 -Confirm:$false '
|
|
f'-ErrorAction SilentlyContinue'
|
|
)
|
|
subprocess.run(
|
|
["powershell", "-NoProfile", "-Command", cmd],
|
|
capture_output=True,
|
|
timeout=20,
|
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
)
|
|
changed.append(name)
|
|
logs.append(f"IPv6 disabled on {name}")
|
|
return changed, logs
|
|
|
|
|
|
def enable_ipv6_on_adapters(adapters: list[str]) -> list[str]:
|
|
if not is_admin() or not adapters:
|
|
return []
|
|
if not _win_probe().has_net_cmdlets:
|
|
return []
|
|
logs: list[str] = []
|
|
for name in adapters:
|
|
cmd = (
|
|
f'Enable-NetAdapterBinding -Name "{name}" -ComponentID ms_tcpip6 -Confirm:$false '
|
|
f'-ErrorAction SilentlyContinue'
|
|
)
|
|
subprocess.run(
|
|
["powershell", "-NoProfile", "-Command", cmd],
|
|
capture_output=True,
|
|
timeout=20,
|
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
)
|
|
logs.append(f"IPv6 re-enabled on {name}")
|
|
return logs
|
|
|
|
|
|
def apply_webrtc_hardening(enable: bool) -> tuple[bool, str]:
|
|
"""Chrome/Edge: disable WebRTC non-proxied UDP (Admin, HKLM policies)."""
|
|
if not is_admin():
|
|
return False, "Administrator required for browser WebRTC policy."
|
|
paths = [_WEBRTC_CHROME, _WEBRTC_EDGE]
|
|
try:
|
|
for path in paths:
|
|
if enable:
|
|
try:
|
|
key = winreg.CreateKeyEx(
|
|
winreg.HKEY_LOCAL_MACHINE, path, 0, winreg.KEY_SET_VALUE
|
|
)
|
|
except OSError:
|
|
continue
|
|
with key:
|
|
winreg.SetValueEx(key, _WEBRTC_VALUE, 0, winreg.REG_DWORD, _WEBRTC_DISABLE)
|
|
else:
|
|
try:
|
|
with winreg.OpenKey(
|
|
winreg.HKEY_LOCAL_MACHINE, path, 0, winreg.KEY_SET_VALUE
|
|
) as key:
|
|
try:
|
|
winreg.DeleteValue(key, _WEBRTC_VALUE)
|
|
except OSError:
|
|
pass
|
|
except OSError:
|
|
pass
|
|
return True, (
|
|
"WebRTC hardened (Chrome/Edge: non-proxied UDP disabled)."
|
|
if enable
|
|
else "WebRTC policy removed."
|
|
)
|
|
except OSError as e:
|
|
return False, str(e)
|
|
|
|
|
|
def _get_os_info() -> str:
|
|
try:
|
|
import platform
|
|
v = platform.version()
|
|
r = platform.release()
|
|
m = platform.machine()
|
|
return f"Windows {r} build {v} {m}"
|
|
except Exception:
|
|
return "unknown"
|
|
|
|
|
|
def _get_timezone() -> str:
|
|
try:
|
|
import datetime
|
|
tz = datetime.datetime.now(datetime.timezone.utc).astimezone()
|
|
name = str(tz.tzname() or "")
|
|
offset = tz.utcoffset()
|
|
h = int(offset.total_seconds() // 3600) if offset else 0
|
|
return f"{name} (UTC{h:+d})" if name else f"UTC{h:+d}"
|
|
except Exception:
|
|
return "unknown"
|
|
|
|
|
|
def _get_screen_resolution() -> str:
|
|
try:
|
|
r = subprocess.run(
|
|
["powershell", "-NoProfile", "-Command",
|
|
"Add-Type -AssemblyName System.Windows.Forms;"
|
|
"[System.Windows.Forms.Screen]::PrimaryScreen.Bounds | "
|
|
"ForEach-Object { \"$($_.Width)x$($_.Height)\" }"],
|
|
capture_output=True, text=True, timeout=8,
|
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
)
|
|
return (r.stdout or "").strip() or "unknown"
|
|
except Exception:
|
|
return "unknown"
|
|
|
|
|
|
def check_browser_fingerprint_consistency(profile_dir: "Path") -> list[str]:
|
|
"""
|
|
Verify that the managed Firefox user.js is internally consistent and
|
|
doesn't mix prefs that would expose the browser as fingerprint-hardened
|
|
while still advertising a normal user-agent.
|
|
|
|
Returns a list of warning strings (empty = consistent).
|
|
"""
|
|
from pathlib import Path
|
|
user_js = Path(profile_dir) / "user.js"
|
|
if not user_js.is_file():
|
|
return ["user.js not found — profile not yet initialized"]
|
|
|
|
try:
|
|
content = user_js.read_text(encoding="utf-8", errors="replace")
|
|
except Exception as e:
|
|
return [f"Cannot read user.js: {e}"]
|
|
|
|
warnings: list[str] = []
|
|
|
|
def _has(pref: str, value: str) -> bool:
|
|
return f'"{pref}", {value}' in content
|
|
|
|
# 1. RFP + custom UA is a contradiction (RFP overrides UA)
|
|
if _has("privacy.resistFingerprinting", "true") and 'general.useragent.override' in content:
|
|
warnings.append(
|
|
"RFP + UA override conflict: privacy.resistFingerprinting=true overrides "
|
|
"general.useragent.override — the spoofed UA is ignored"
|
|
)
|
|
|
|
# 2. proxy not set but force_proxy claimed
|
|
if _has("network.proxy.type", "1"):
|
|
if 'network.proxy.http",' not in content:
|
|
warnings.append("proxy.type=1 but network.proxy.http is missing — browsers will error")
|
|
|
|
# 3. FPI on + third-party cookies allowed = inconsistent privacy posture
|
|
if _has("privacy.firstparty.isolate", "true") and _has("network.cookie.cookieBehavior", "0"):
|
|
warnings.append(
|
|
"FPI enabled but cookieBehavior=0 (accept all) — cookies are isolated but not blocked"
|
|
)
|
|
|
|
# 4. sanitizeOnShutdown without clearing history = incomplete wipe
|
|
if _has("privacy.sanitize.sanitizeOnShutdown", "true"):
|
|
if not _has("privacy.clearOnShutdown.history", "true"):
|
|
warnings.append(
|
|
"sanitizeOnShutdown=true but clearOnShutdown.history not set — "
|
|
"history may survive session"
|
|
)
|
|
|
|
# 5. WebRTC peerconnection disabled is good — flag if missing
|
|
if not _has("media.peerconnection.enabled", "false"):
|
|
warnings.append("WebRTC not disabled in profile — real IP can leak via STUN")
|
|
|
|
# 6. network.trr.mode=5 (DoH off) is expected when using proxy DNS
|
|
if not _has("network.trr.mode", "5"):
|
|
warnings.append(
|
|
"network.trr.mode is not 5 — Firefox may use its own DoH resolver, "
|
|
"bypassing the proxy chain DNS path"
|
|
)
|
|
|
|
return warnings
|
|
|
|
|
|
def audit_device() -> FingerprintAudit:
|
|
"""Collect OS-level identifiers and consistency notes."""
|
|
import getpass
|
|
import os
|
|
from pathlib import Path
|
|
|
|
host = get_computer_name()
|
|
guid = get_machine_guid()
|
|
user = getpass.getuser()
|
|
nics = list_nics()
|
|
macs = [f"{n.name}: {n.mac}" for n in nics]
|
|
os_ = _get_os_info()
|
|
tz_ = _get_timezone()
|
|
res_ = _get_screen_resolution()
|
|
|
|
lines: list[str] = [
|
|
"── OS / Identity ──────────────────────────────────",
|
|
f" OS : {os_}",
|
|
f" Computer name: {host or '—'}",
|
|
f" Username : {user}",
|
|
f" MachineGuid : " + (f"{guid[:8]}…{guid[-4:]}" if len(guid) > 12 else guid or "—"),
|
|
f" Timezone : {tz_}",
|
|
f" Screen res : {res_}",
|
|
"",
|
|
"── Network adapters ───────────────────────────────",
|
|
]
|
|
lines.extend([f" • {m}" for m in macs[:10]] or [" • (none detected)"])
|
|
if len(macs) > 10:
|
|
lines.append(f" … and {len(macs) - 10} more")
|
|
|
|
lines += [
|
|
"",
|
|
"── Browser fingerprint note ───────────────────────",
|
|
" Canvas, WebGL, font metrics, audio context, and screen dimensions",
|
|
" are NOT changed at the OS level by this app.",
|
|
" → Use 'blend_windows_chrome' persona OR 'hardened' mode to control",
|
|
" what the browser reports to sites.",
|
|
" → RFP (Resist Fingerprinting) makes Firefox stand out as hardened.",
|
|
" → Blend persona spoofs UA/platform to look like a normal Windows Chrome.",
|
|
"",
|
|
"── Recommendations ────────────────────────────────",
|
|
]
|
|
|
|
# Quick consistency checks
|
|
webrtc_ok, _ = apply_webrtc_hardening.__doc__ and True or False
|
|
try:
|
|
import winreg as _wr
|
|
with _wr.OpenKey(_wr.HKEY_LOCAL_MACHINE, r"SOFTWARE\Policies\Google\Chrome",
|
|
0, _wr.KEY_QUERY_VALUE) as k:
|
|
v = int(_wr.QueryValueEx(k, "DefaultWebRtcIpHandlingPolicy")[0])
|
|
webrtc_ok = v == 2
|
|
except Exception:
|
|
webrtc_ok = False
|
|
|
|
if not webrtc_ok:
|
|
lines.append(" ⚠ Chrome/Edge WebRTC policy not set — enable in Privacy tab")
|
|
else:
|
|
lines.append(" ✓ Chrome/Edge WebRTC policy is set")
|
|
|
|
# Check IPv6
|
|
try:
|
|
r = subprocess.run(
|
|
["powershell", "-NoProfile", "-NonInteractive", "-Command",
|
|
"Get-NetAdapterBinding -ComponentID ms_tcpip6 | Where-Object { $_.Enabled } | Measure-Object | Select-Object -ExpandProperty Count"],
|
|
capture_output=True, text=True, timeout=8,
|
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
)
|
|
count = int((r.stdout or "0").strip() or "0")
|
|
if count > 0:
|
|
lines.append(f" ⚠ IPv6 active on {count} adapter(s) — disable in Privacy tab for full v4-only chain")
|
|
else:
|
|
lines.append(" ✓ IPv6 disabled on all adapters")
|
|
except Exception:
|
|
lines.append(" ? IPv6 status unknown")
|
|
|
|
return FingerprintAudit(
|
|
lines=lines,
|
|
hostname=host,
|
|
machine_guid=guid,
|
|
username=user,
|
|
macs=[n.mac for n in nics],
|
|
)
|