Skip to content

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

// Browser Fingerprint — pure logic (Java port): the 18-signal privacy-risk
// registry with canonical grouping, risk counts, and a stable SHA-256
// fingerprint hash.
//
// Language: Java 17 — standard library only (java.security for SHA-256,
//           java.util.HexFormat for hex output). Save as BrowserFingerprint.java.
// 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.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;

public final class BrowserFingerprint {

    /** Privacy-risk classification. */
    public enum RiskLevel {
        /** 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");

        private final String label;

        RiskLevel(String label) {
            this.label = label;
        }

        public String label() {
            return label;
        }
    }

    /** Groups related signals. */
    public enum FingerprintCategory {
        HARDWARE("Hardware"),
        GRAPHICS("Graphics"),
        NETWORK("Network"),
        BROWSER("Browser"),
        INPUT("Input");

        private final String label;

        FingerprintCategory(String label) {
            this.label = label;
        }

        public String label() {
            return label;
        }
    }

    /** One collected, classified signal. */
    public record FingerprintSignal(String id, String label, String value, RiskLevel risk,
            FingerprintCategory category) {
    }

    /** Classifies one signal id. */
    private record SignalDef(String label, RiskLevel risk, FingerprintCategory category) {
    }

    /** One category and its signals. */
    public record CategoryGroup(FingerprintCategory category, List<FingerprintSignal> signals) {
    }

    /** Signals per risk level (drives the summary line). */
    public record RiskCounts(int low, int medium, int high) {
    }

    /** Canonical category order (card grid, top to bottom). */
    private static final FingerprintCategory[] FINGERPRINT_CATEGORIES = {
        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.
     */
    private static final Map<String, SignalDef> SIGNAL_DEFINITIONS = Map.ofEntries(
            // High risk
            Map.entry("canvas", new SignalDef("Canvas fingerprint", RiskLevel.HIGH, FingerprintCategory.GRAPHICS)),
            Map.entry("webgl-renderer", new SignalDef("WebGL renderer", RiskLevel.HIGH, FingerprintCategory.GRAPHICS)),
            Map.entry("webgl-vendor", new SignalDef("WebGL vendor", RiskLevel.HIGH, FingerprintCategory.GRAPHICS)),
            Map.entry("fonts", new SignalDef("Installed fonts", RiskLevel.HIGH, FingerprintCategory.BROWSER)),
            Map.entry("timezone", new SignalDef("Timezone", RiskLevel.HIGH, FingerprintCategory.BROWSER)),
            // Medium risk
            Map.entry("screen", new SignalDef("Screen & color depth", RiskLevel.MEDIUM, FingerprintCategory.HARDWARE)),
            Map.entry("device-pixel-ratio", new SignalDef("Device pixel ratio", RiskLevel.MEDIUM, FingerprintCategory.HARDWARE)),
            Map.entry("hardware-concurrency", new SignalDef("CPU cores", RiskLevel.MEDIUM, FingerprintCategory.HARDWARE)),
            Map.entry("device-memory", new SignalDef("Device Memory", RiskLevel.MEDIUM, FingerprintCategory.HARDWARE)),
            Map.entry("platform", new SignalDef("Platform", RiskLevel.MEDIUM, FingerprintCategory.BROWSER)),
            Map.entry("languages", new SignalDef("Languages", RiskLevel.MEDIUM, FingerprintCategory.BROWSER)),
            Map.entry("touch", new SignalDef("Touch support", RiskLevel.MEDIUM, FingerprintCategory.INPUT)),
            Map.entry("connection", new SignalDef("Connection type", RiskLevel.MEDIUM, FingerprintCategory.NETWORK)),
            // Low risk
            Map.entry("user-agent", new SignalDef("User agent", RiskLevel.LOW, FingerprintCategory.BROWSER)),
            Map.entry("do-not-track", new SignalDef("Do Not Track", RiskLevel.LOW, FingerprintCategory.BROWSER)),
            Map.entry("cookies-enabled", new SignalDef("Cookies enabled", RiskLevel.LOW, FingerprintCategory.BROWSER)),
            Map.entry("online", new SignalDef("Online status", RiskLevel.LOW, FingerprintCategory.NETWORK)),
            Map.entry("pdf-viewer", new SignalDef("PDF viewer", RiskLevel.LOW, FingerprintCategory.BROWSER)));

    /** Risk level for a known signal id; null for unknown ids. */
    public static RiskLevel classifySignalRisk(String id) {
        SignalDef def = SIGNAL_DEFINITIONS.get(id);
        return def == null ? null : def.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.
     */
    public static FingerprintSignal buildSignal(String id, String value) {
        SignalDef def = SIGNAL_DEFINITIONS.get(id);
        if (def == null) {
            throw new IllegalArgumentException("Unknown fingerprint signal id: " + id);
        }
        return new FingerprintSignal(id, def.label(), value, def.risk(), def.category());
    }

    /** Group signals by category in canonical order, omitting empty categories. */
    public static List<CategoryGroup> groupByCategory(List<FingerprintSignal> signals) {
        List<CategoryGroup> groups = new ArrayList<>();
        for (FingerprintCategory category : FINGERPRINT_CATEGORIES) {
            List<FingerprintSignal> members = new ArrayList<>();
            for (FingerprintSignal signal : signals) {
                if (signal.category() == category) {
                    members.add(signal);
                }
            }
            if (!members.isEmpty()) {
                groups.add(new CategoryGroup(category, members));
            }
        }
        return groups;
    }

    /** Count signals per risk level. */
    public static RiskCounts countByRisk(List<FingerprintSignal> signals) {
        int low = 0;
        int medium = 0;
        int high = 0;
        for (FingerprintSignal signal : signals) {
            switch (signal.risk()) {
                case LOW -> low++;
                case MEDIUM -> medium++;
                case HIGH -> high++;
            }
        }
        return new RiskCounts(low, medium, high);
    }

    /**
     * The exact byte string the fingerprint hash is computed over: every value
     * joined with '|' in signal order.
     */
    public static String concatSignalValues(List<FingerprintSignal> signals) {
        StringBuilder joined = new StringBuilder();
        for (FingerprintSignal signal : signals) {
            if (joined.length() > 0) {
                joined.append('|');
            }
            joined.append(signal.value());
        }
        return joined.toString();
    }

    /** SHA-256 hex digest of the UTF-8 bytes of input. */
    public static String sha256Hex(String input) {
        try {
            byte[] digest = MessageDigest.getInstance("SHA-256")
                    .digest(input.getBytes(StandardCharsets.UTF_8));
            return HexFormat.of().formatHex(digest);
        } catch (NoSuchAlgorithmException e) {
            // Every Java platform ships SHA-256; unreachable in practice.
            throw new IllegalStateException("SHA-256 unavailable", e);
        }
    }

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

    public static void main(String[] args) {
        List<FingerprintSignal> signals = List.of(
                buildSignal("timezone", "Europe/Paris"),
                buildSignal("screen", "2560×1440 @ 24-bit"),
                buildSignal("user-agent", "Mozilla/5.0 (Macintosh)"));

        for (CategoryGroup group : groupByCategory(signals)) {
            System.out.println("[" + group.category().label() + "]");
            for (FingerprintSignal signal : group.signals()) {
                System.out.println("  " + signal.label() + ": " + signal.value()
                        + " (" + signal.risk().label() + " risk)");
            }
        }

        RiskCounts counts = countByRisk(signals);
        System.out.println("risk mix: " + counts.high() + " high / " + counts.medium()
                + " medium / " + counts.low() + " low");
        System.out.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 →