Skip to content

Browser Fingerprint Viewer — Kotlin 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 Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Browser Fingerprint — pure logic (Kotlin port): the 18-signal privacy-risk
// registry with canonical grouping, risk counts, and a stable SHA-256
// fingerprint hash.
//
// Language: Kotlin 1.9 (JVM, Java 17 runtime) — standard library only;
//           SHA-256 via java.security.MessageDigest, hex via java.util.HexFormat.
// Source: CosmoDev polyglot showcase port; canonical = src/lib/browser-fingerprint.ts
//         + this tool's python.py / rust.rs.
// License: display source — part of CosmoDev's polyglot tool pages.
//
// Ported from the TypeScript reference. 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.

import java.security.MessageDigest
import java.util.HexFormat

/** Privacy-risk classification. */
enum class RiskLevel(val label: String) {
    /** Coarse; shared by millions of browsers. */
    LOW("low"),

    /** Narrows you to a sizable-but-specific population. */
    MEDIUM("medium"),

    /** Near-unique on its own or part of a rare combination. */
    HIGH("high"),
}

/** Groups related signals. */
enum class FingerprintCategory(val label: String) {
    HARDWARE("Hardware"),
    GRAPHICS("Graphics"),
    NETWORK("Network"),
    BROWSER("Browser"),
    INPUT("Input"),
}

/** One collected, classified signal. */
data class FingerprintSignal(
    val id: String,
    val label: String,
    val value: String,
    val risk: RiskLevel,
    val category: FingerprintCategory,
)

/** Classifies one signal id. */
data class SignalDef(
    val label: String,
    val risk: RiskLevel,
    val category: FingerprintCategory,
)

/** One category and its signals. */
data class CategoryGroup(
    val category: FingerprintCategory,
    val signals: List<FingerprintSignal>,
)

/** Signals per risk level (drives the summary line). */
data class RiskCounts(
    val low: Int = 0,
    val medium: Int = 0,
    val high: Int = 0,
)

/** Canonical category order (card grid, top to bottom). */
val FINGERPRINT_CATEGORIES: List<FingerprintCategory> = listOf(
    FingerprintCategory.HARDWARE,
    FingerprintCategory.GRAPHICS,
    FingerprintCategory.NETWORK,
    FingerprintCategory.BROWSER,
    FingerprintCategory.INPUT,
)

/**
 * Static registry of every signal the tool collects, with its privacy-risk
 * classification. Single source of truth: [buildSignal] derives from it, so a
 * signal can never be collected without being classified.
 */
val SIGNAL_DEFINITIONS: Map<String, SignalDef> = mapOf(
    // High risk
    "canvas" to SignalDef("Canvas fingerprint", RiskLevel.HIGH, FingerprintCategory.GRAPHICS),
    "webgl-renderer" to SignalDef("WebGL renderer", RiskLevel.HIGH, FingerprintCategory.GRAPHICS),
    "webgl-vendor" to SignalDef("WebGL vendor", RiskLevel.HIGH, FingerprintCategory.GRAPHICS),
    "fonts" to SignalDef("Installed fonts", RiskLevel.HIGH, FingerprintCategory.BROWSER),
    "timezone" to SignalDef("Timezone", RiskLevel.HIGH, FingerprintCategory.BROWSER),
    // Medium risk
    "screen" to SignalDef("Screen & color depth", RiskLevel.MEDIUM, FingerprintCategory.HARDWARE),
    "device-pixel-ratio" to SignalDef("Device pixel ratio", RiskLevel.MEDIUM, FingerprintCategory.HARDWARE),
    "hardware-concurrency" to SignalDef("CPU cores", RiskLevel.MEDIUM, FingerprintCategory.HARDWARE),
    "device-memory" to SignalDef("Device Memory", RiskLevel.MEDIUM, FingerprintCategory.HARDWARE),
    "platform" to SignalDef("Platform", RiskLevel.MEDIUM, FingerprintCategory.BROWSER),
    "languages" to SignalDef("Languages", RiskLevel.MEDIUM, FingerprintCategory.BROWSER),
    "touch" to SignalDef("Touch support", RiskLevel.MEDIUM, FingerprintCategory.INPUT),
    "connection" to SignalDef("Connection type", RiskLevel.MEDIUM, FingerprintCategory.NETWORK),
    // Low risk
    "user-agent" to SignalDef("User agent", RiskLevel.LOW, FingerprintCategory.BROWSER),
    "do-not-track" to SignalDef("Do Not Track", RiskLevel.LOW, FingerprintCategory.BROWSER),
    "cookies-enabled" to SignalDef("Cookies enabled", RiskLevel.LOW, FingerprintCategory.BROWSER),
    "online" to SignalDef("Online status", RiskLevel.LOW, FingerprintCategory.NETWORK),
    "pdf-viewer" to SignalDef("PDF viewer", RiskLevel.LOW, FingerprintCategory.BROWSER),
)

/** Risk level for a known signal id; null for unknown ids. */
fun classifySignalRisk(id: String): RiskLevel? = SIGNAL_DEFINITIONS[id]?.risk

/**
 * Build a signal from its id and collected value. Throws
 * [IllegalArgumentException] on unknown ids so a typo'd id fails loudly
 * instead of silently rendering an unclassified row.
 */
fun buildSignal(id: String, value: String): FingerprintSignal {
    val def = SIGNAL_DEFINITIONS[id]
        ?: throw IllegalArgumentException("Unknown fingerprint signal id: $id")
    return FingerprintSignal(id, def.label, value, def.risk, def.category)
}

/** Group signals by category in canonical order, omitting empty categories. */
fun groupByCategory(signals: List<FingerprintSignal>): List<CategoryGroup> =
    FINGERPRINT_CATEGORIES.mapNotNull { category ->
        signals.filter { it.category == category }
            .takeIf { it.isNotEmpty() }
            ?.let { CategoryGroup(category, it) }
    }

/** Count signals per risk level. */
fun countByRisk(signals: List<FingerprintSignal>): RiskCounts {
    var low = 0
    var medium = 0
    var high = 0
    for (signal in signals) {
        when (signal.risk) {
            RiskLevel.LOW -> low++
            RiskLevel.MEDIUM -> medium++
            RiskLevel.HIGH -> high++
        }
    }
    return RiskCounts(low, medium, high)
}

/**
 * The exact byte string the fingerprint hash is computed over: every value
 * joined with '|' in signal order.
 */
fun concatSignalValues(signals: List<FingerprintSignal>): String =
    signals.joinToString(separator = "|") { it.value }

/** SHA-256 hex digest of the UTF-8 bytes of input. */
fun sha256Hex(input: String): String {
    val digest = MessageDigest.getInstance("SHA-256").digest(input.toByteArray(Charsets.UTF_8))
    return HexFormat.of().formatHex(digest)
}

/**
 * Stable fingerprint ID: SHA-256 over every signal value, joined in signal
 * order. Same browser state -> same hash; any single changed value -> new hash.
 */
fun hashFingerprint(signals: List<FingerprintSignal>): String =
    sha256Hex(concatSignalValues(signals))

fun main() {
    val signals = listOf(
        buildSignal("timezone", "Europe/Paris"),
        buildSignal("screen", "2560×1440 @ 24-bit"),
        buildSignal("user-agent", "Mozilla/5.0 (Macintosh)"),
    )

    for (group in groupByCategory(signals)) {
        println("[${group.category.label}]")
        for (signal in group.signals) {
            println("  ${signal.label}: ${signal.value} (${signal.risk.label} risk)")
        }
    }

    val counts = countByRisk(signals)
    println("risk mix: ${counts.high} high / ${counts.medium} medium / ${counts.low} low")
    println("fingerprint: ${hashFingerprint(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 →