"""Device fingerprint audit and OS-level hardening helpers.""" from __future__ import annotations import logging import platform 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" # Legacy DWORD key — value 3 = disable_non_proxied_udp (value 2 was wrong: public+private only) _WEBRTC_VALUE = "DefaultWebRtcIpHandlingPolicy" _WEBRTC_DISABLE = 3 # disable_non_proxied_udp (correct Chrome/Edge DWORD) # Modern REG_SZ key required by Chrome 114+ Group Policy _WEBRTC_VALUE_STR = "WebRtcIPHandling" _WEBRTC_DISABLE_STR = "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 platform.node() 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). Writes both the legacy DWORD key (DefaultWebRtcIpHandlingPolicy=3) and the modern REG_SZ key (WebRtcIPHandling=disable_non_proxied_udp) so that all Chrome/Edge versions are covered. """ 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) winreg.SetValueEx(key, _WEBRTC_VALUE_STR, 0, winreg.REG_SZ, _WEBRTC_DISABLE_STR) else: try: with winreg.OpenKey( winreg.HKEY_LOCAL_MACHINE, path, 0, winreg.KEY_SET_VALUE ) as key: for val_name in (_WEBRTC_VALUE, _WEBRTC_VALUE_STR): try: winreg.DeleteValue(key, val_name) 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: return f"{platform.system()} {platform.release()} {platform.machine()}" 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: if platform.system() == "Darwin": try: r = subprocess.run( ["system_profiler", "SPDisplaysDataType"], capture_output=True, text=True, timeout=8, ) for line in (r.stdout or "").splitlines(): if "Resolution:" in line: return line.split("Resolution:", 1)[1].strip() except Exception: return "unknown" 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 IP-handling policy (Chrome / Edge) # Accept either: DWORD DefaultWebRtcIpHandlingPolicy==3 # or REG_SZ WebRtcIPHandling=="disable_non_proxied_udp" webrtc_ok = False for hive_root in (_WEBRTC_CHROME, _WEBRTC_EDGE): try: with winreg.OpenKey( winreg.HKEY_LOCAL_MACHINE, hive_root, 0, winreg.KEY_QUERY_VALUE ) as k: try: v = int(winreg.QueryValueEx(k, _WEBRTC_VALUE)[0]) if v == _WEBRTC_DISABLE: webrtc_ok = True break except OSError: pass try: v_str = str(winreg.QueryValueEx(k, _WEBRTC_VALUE_STR)[0]) if v_str == _WEBRTC_DISABLE_STR: webrtc_ok = True break except OSError: pass except OSError: continue if webrtc_ok: lines.append(" ✓ Chrome/Edge WebRTC IP-handling policy is set (disable_non_proxied_udp)") else: lines.append(" ⚠ Chrome/Edge WebRTC policy not set — enable in Privacy tab") # 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], )