first commit

This commit is contained in:
drjones
2026-05-23 21:58:06 -07:00
commit 1f5e63ca1f
73 changed files with 13565 additions and 0 deletions

View File

@@ -0,0 +1,416 @@
"""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],
)