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 →