Browser Fingerprint Viewer — Python source
See exactly what websites can learn about your browser without cookies — screen, GPU, fonts, timezone, language, and more. Educational, not tracking.
This is the Python implementation — the same logic the interactive tool runs, in a shareable, citable form.
"""browser-fingerprint — Python polyglot showcase port (pure analysis layer).
Ported from the TypeScript reference at src/lib/browser-fingerprint.ts.
Covers the portable core: the 18-signal registry with privacy-risk
classifications, canonical category grouping, risk counts, and the stable
SHA-256 fingerprint hash. The browser-API collection layer (canvas / WebGL /
font probes) is DOM-bound by design and does not port.
Display source — part of CosmoDev's polyglot tool pages.
"""
from __future__ import annotations
import hashlib
from dataclasses import dataclass, field
from enum import Enum
class RiskLevel(str, Enum):
"""Privacy-risk classification.
HIGH: near-unique on its own or part of a rare combination.
MEDIUM: narrows you to a sizable-but-specific population.
LOW: coarse; shared by millions of browsers.
"""
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
class FingerprintCategory(str, Enum):
"""Groups related signals."""
HARDWARE = "Hardware"
GRAPHICS = "Graphics"
NETWORK = "Network"
BROWSER = "Browser"
INPUT = "Input"
#: Canonical category order (card grid, top to bottom).
FINGERPRINT_CATEGORIES: list[FingerprintCategory] = [
FingerprintCategory.HARDWARE,
FingerprintCategory.GRAPHICS,
FingerprintCategory.NETWORK,
FingerprintCategory.BROWSER,
FingerprintCategory.INPUT,
]
@dataclass(frozen=True)
class SignalDef:
"""Classifies one signal id."""
label: str
risk: RiskLevel
category: FingerprintCategory
@dataclass
class FingerprintSignal:
"""One collected, classified signal."""
id: str
label: str
value: str
risk: RiskLevel
category: FingerprintCategory
#: Static registry of every signal the tool collects, with its privacy-risk
#: classification. Single source of truth: build_signal() derives from it, so
#: a signal can never be collected without being classified.
SIGNAL_DEFINITIONS: dict[str, SignalDef] = {
# High risk
"canvas": SignalDef("Canvas fingerprint", RiskLevel.HIGH, FingerprintCategory.GRAPHICS),
"webgl-renderer": SignalDef("WebGL renderer", RiskLevel.HIGH, FingerprintCategory.GRAPHICS),
"webgl-vendor": SignalDef("WebGL vendor", RiskLevel.HIGH, FingerprintCategory.GRAPHICS),
"fonts": SignalDef("Installed fonts", RiskLevel.HIGH, FingerprintCategory.BROWSER),
"timezone": SignalDef("Timezone", RiskLevel.HIGH, FingerprintCategory.BROWSER),
# Medium risk
"screen": SignalDef("Screen & color depth", RiskLevel.MEDIUM, FingerprintCategory.HARDWARE),
"device-pixel-ratio": SignalDef("Device pixel ratio", RiskLevel.MEDIUM, FingerprintCategory.HARDWARE),
"hardware-concurrency": SignalDef("CPU cores", RiskLevel.MEDIUM, FingerprintCategory.HARDWARE),
"device-memory": SignalDef("Device Memory", RiskLevel.MEDIUM, FingerprintCategory.HARDWARE),
"platform": SignalDef("Platform", RiskLevel.MEDIUM, FingerprintCategory.BROWSER),
"languages": SignalDef("Languages", RiskLevel.MEDIUM, FingerprintCategory.BROWSER),
"touch": SignalDef("Touch support", RiskLevel.MEDIUM, FingerprintCategory.INPUT),
"connection": SignalDef("Connection type", RiskLevel.MEDIUM, FingerprintCategory.NETWORK),
# Low risk
"user-agent": SignalDef("User agent", RiskLevel.LOW, FingerprintCategory.BROWSER),
"do-not-track": SignalDef("Do Not Track", RiskLevel.LOW, FingerprintCategory.BROWSER),
"cookies-enabled": SignalDef("Cookies enabled", RiskLevel.LOW, FingerprintCategory.BROWSER),
"online": SignalDef("Online status", RiskLevel.LOW, FingerprintCategory.NETWORK),
"pdf-viewer": SignalDef("PDF viewer", RiskLevel.LOW, FingerprintCategory.BROWSER),
}
@dataclass
class CategoryGroup:
"""One category and its signals."""
category: FingerprintCategory
signals: list[FingerprintSignal] = field(default_factory=list)
@dataclass
class RiskCounts:
"""Signals per risk level (drives the summary line)."""
low: int = 0
medium: int = 0
high: int = 0
def classify_signal_risk(signal_id: str) -> RiskLevel | None:
"""Risk level for a known signal id; None for unknown ids."""
definition = SIGNAL_DEFINITIONS.get(signal_id)
return definition.risk if definition else None
def build_signal(signal_id: str, value: str) -> FingerprintSignal:
"""Build a signal from its id and collected value.
Raises ValueError on unknown ids so a typo'd id fails loudly instead of
silently rendering an unclassified row.
"""
try:
definition = SIGNAL_DEFINITIONS[signal_id]
except KeyError:
raise ValueError(f"Unknown fingerprint signal id: {signal_id}") from None
return FingerprintSignal(
id=signal_id,
label=definition.label,
value=value,
risk=definition.risk,
category=definition.category,
)
def group_by_category(signals: list[FingerprintSignal]) -> list[CategoryGroup]:
"""Group signals by category in canonical order, omitting empty categories."""
groups: dict[FingerprintCategory, CategoryGroup] = {}
for signal in signals:
group = groups.get(signal.category)
if group is None:
group = groups[signal.category] = CategoryGroup(signal.category)
group.signals.append(signal)
return [groups[c] for c in FINGERPRINT_CATEGORIES if c in groups]
def count_by_risk(signals: list[FingerprintSignal]) -> RiskCounts:
"""Count signals per risk level."""
counts = RiskCounts()
for signal in signals:
setattr(counts, signal.risk.value, getattr(counts, signal.risk.value) + 1)
return counts
def concat_signal_values(signals: list[FingerprintSignal]) -> str:
"""The exact byte string the fingerprint hash is computed over."""
return "|".join(s.value for s in signals)
def sha256_hex(text: str) -> str:
"""SHA-256 hex digest of the UTF-8 encoding of text."""
return hashlib.sha256(text.encode("utf-8")).hexdigest()
def hash_fingerprint(signals: list[FingerprintSignal]) -> str:
"""Stable fingerprint ID: SHA-256 over every signal value, joined in
signal order. Same browser state -> same hash; any single changed
value -> new hash."""
return sha256_hex(concat_signal_values(signals))
if __name__ == "__main__":
signals = [
build_signal("timezone", "Europe/Paris"),
build_signal("screen", "2560×1440 @ 24-bit"),
build_signal("user-agent", "Mozilla/5.0 (Macintosh)"),
]
for group in group_by_category(signals):
print(f"[{group.category.value}]")
for s in group.signals:
print(f" {s.label}: {s.value} ({s.risk.value} risk)")
counts = count_by_risk(signals)
print(f"risk mix: {counts.high} high / {counts.medium} medium / {counts.low} low")
print(f"fingerprint: {hash_fingerprint(signals)}")
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →