"""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 audit_device() -> FingerprintAudit: """Collect identifiers sites and trackers often fingerprint.""" import getpass import os host = get_computer_name() guid = get_machine_guid() user = getpass.getuser() macs = [f"{n.name}: {n.mac}" for n in list_nics()] lines = [ f"Computer name: {host or '—'}", f"Windows username: {user}", f"MachineGuid: {guid[:8]}…{guid[-4:]}" if len(guid) > 12 else f"MachineGuid: {guid or '—'}", f"Network adapters ({len(macs)}):", ] lines.extend([f" • {m}" for m in macs[:8]] or [" • (none detected)"]) if len(macs) > 8: lines.append(f" … and {len(macs) - 8} more") lines.append("") lines.append("Browser fingerprint (Canvas/WebGL/fonts) is not changed by this app.") lines.append("Use hardened browser profiles + proxy chain for web traffic.") lines.append("WebRTC toggle here affects Chrome/Edge system policy only.") return FingerprintAudit( lines=lines, hostname=host, machine_guid=guid, username=user, macs=[n.mac for n in list_nics()], )