Skip to content

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 →