Skip to content

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

// Browser Fingerprint — pure logic (C# port): the 18-signal privacy-risk
// registry with canonical grouping, risk counts, and a stable SHA-256
// fingerprint hash.
//
// Language: C# 12 (.NET 8) — standard library only; SHA-256 comes from
//           System.Security.Cryptography's one-shot SHA256.HashData.
// 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.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

namespace BrowserFingerprint;

/// <summary>Privacy-risk classification.</summary>
public enum RiskLevel
{
    /// <summary>Coarse; shared by millions of browsers.</summary>
    Low,

    /// <summary>Narrows you to a sizable-but-specific population.</summary>
    Medium,

    /// <summary>Near-unique on its own or part of a rare combination.</summary>
    High,
}

public static class RiskLevelExtensions
{
    public static string AsStr(this RiskLevel risk) => risk switch
    {
        RiskLevel.Low => "low",
        RiskLevel.Medium => "medium",
        RiskLevel.High => "high",
        _ => throw new ArgumentOutOfRangeException(nameof(risk)),
    };
}

/// <summary>Groups related signals.</summary>
public enum FingerprintCategory
{
    Hardware,
    Graphics,
    Network,
    Browser,
    Input,
}

public static class FingerprintCategoryExtensions
{
    public static string AsStr(this FingerprintCategory category) => category switch
    {
        FingerprintCategory.Hardware => "Hardware",
        FingerprintCategory.Graphics => "Graphics",
        FingerprintCategory.Network => "Network",
        FingerprintCategory.Browser => "Browser",
        FingerprintCategory.Input => "Input",
        _ => throw new ArgumentOutOfRangeException(nameof(category)),
    };
}

/// <summary>One collected, classified signal.</summary>
public sealed record FingerprintSignal(
    string Id,
    string Label,
    string Value,
    RiskLevel Risk,
    FingerprintCategory Category);

/// <summary>Classifies one signal id.</summary>
public sealed record SignalDef(string Label, RiskLevel Risk, FingerprintCategory Category);

/// <summary>One category and its signals.</summary>
public sealed record CategoryGroup(FingerprintCategory Category, IReadOnlyList<FingerprintSignal> Signals);

/// <summary>Signals per risk level (drives the summary line).</summary>
public sealed record RiskCounts(int Low, int Medium, int High);

public static class Fingerprint
{
    /// <summary>Canonical category order (card grid, top to bottom).</summary>
    public static readonly FingerprintCategory[] Categories =
    [
        FingerprintCategory.Hardware,
        FingerprintCategory.Graphics,
        FingerprintCategory.Network,
        FingerprintCategory.Browser,
        FingerprintCategory.Input,
    ];

    /// <summary>
    /// Static registry of every signal the tool collects, with its privacy-risk
    /// classification. Single source of truth: <see cref="BuildSignal"/> derives
    /// from it, so a signal can never be collected without being classified.
    /// </summary>
    public static readonly IReadOnlyDictionary<string, SignalDef> SignalDefinitions =
        new Dictionary<string, SignalDef>
        {
            // High risk
            ["canvas"] = new("Canvas fingerprint", RiskLevel.High, FingerprintCategory.Graphics),
            ["webgl-renderer"] = new("WebGL renderer", RiskLevel.High, FingerprintCategory.Graphics),
            ["webgl-vendor"] = new("WebGL vendor", RiskLevel.High, FingerprintCategory.Graphics),
            ["fonts"] = new("Installed fonts", RiskLevel.High, FingerprintCategory.Browser),
            ["timezone"] = new("Timezone", RiskLevel.High, FingerprintCategory.Browser),
            // Medium risk
            ["screen"] = new("Screen & color depth", RiskLevel.Medium, FingerprintCategory.Hardware),
            ["device-pixel-ratio"] = new("Device pixel ratio", RiskLevel.Medium, FingerprintCategory.Hardware),
            ["hardware-concurrency"] = new("CPU cores", RiskLevel.Medium, FingerprintCategory.Hardware),
            ["device-memory"] = new("Device Memory", RiskLevel.Medium, FingerprintCategory.Hardware),
            ["platform"] = new("Platform", RiskLevel.Medium, FingerprintCategory.Browser),
            ["languages"] = new("Languages", RiskLevel.Medium, FingerprintCategory.Browser),
            ["touch"] = new("Touch support", RiskLevel.Medium, FingerprintCategory.Input),
            ["connection"] = new("Connection type", RiskLevel.Medium, FingerprintCategory.Network),
            // Low risk
            ["user-agent"] = new("User agent", RiskLevel.Low, FingerprintCategory.Browser),
            ["do-not-track"] = new("Do Not Track", RiskLevel.Low, FingerprintCategory.Browser),
            ["cookies-enabled"] = new("Cookies enabled", RiskLevel.Low, FingerprintCategory.Browser),
            ["online"] = new("Online status", RiskLevel.Low, FingerprintCategory.Network),
            ["pdf-viewer"] = new("PDF viewer", RiskLevel.Low, FingerprintCategory.Browser),
        };

    /// <summary>Risk level for a known signal id; null for unknown ids.</summary>
    public static RiskLevel? ClassifySignalRisk(string id) =>
        SignalDefinitions.TryGetValue(id, out var def) ? def.Risk : null;

    /// <summary>
    /// Build a signal from its id and collected value. Throws
    /// <see cref="ArgumentException"/> on unknown ids so a typo'd id fails
    /// loudly instead of silently rendering an unclassified row.
    /// </summary>
    public static FingerprintSignal BuildSignal(string id, string value)
    {
        if (!SignalDefinitions.TryGetValue(id, out var def))
        {
            throw new ArgumentException($"Unknown fingerprint signal id: {id}", nameof(id));
        }
        return new FingerprintSignal(id, def.Label, value, def.Risk, def.Category);
    }

    /// <summary>Group signals by category in canonical order, omitting empty categories.</summary>
    public static List<CategoryGroup> GroupByCategory(IEnumerable<FingerprintSignal> signals)
    {
        var byCategory = signals.ToLookup(signal => signal.Category);
        return
        [
            .. Categories
                .Where(category => byCategory[category].Any())
                .Select(category => new CategoryGroup(category, byCategory[category].ToList())),
        ];
    }

    /// <summary>Count signals per risk level.</summary>
    public static RiskCounts CountByRisk(IEnumerable<FingerprintSignal> signals)
    {
        int low = 0, medium = 0, high = 0;
        foreach (var signal in signals)
        {
            switch (signal.Risk)
            {
                case RiskLevel.Low:
                    low++;
                    break;
                case RiskLevel.Medium:
                    medium++;
                    break;
                case RiskLevel.High:
                    high++;
                    break;
            }
        }
        return new RiskCounts(low, medium, high);
    }

    /// <summary>The exact byte string the fingerprint hash is computed over:
    /// every value joined with '|' in signal order.</summary>
    public static string ConcatSignalValues(IEnumerable<FingerprintSignal> signals) =>
        string.Join("|", signals.Select(signal => signal.Value));

    /// <summary>SHA-256 hex digest of the UTF-8 bytes of input.</summary>
    public static string Sha256Hex(string input) =>
        Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input))).ToLowerInvariant();

    /// <summary>
    /// Stable fingerprint ID: SHA-256 over every signal value, joined in signal
    /// order. Same browser state -> same hash; any single changed value -> new hash.
    /// </summary>
    public static string HashFingerprint(IReadOnlyList<FingerprintSignal> signals) =>
        Sha256Hex(ConcatSignalValues(signals));
}

public static class Program
{
    public static void Main()
    {
        var signals = new List<FingerprintSignal>
        {
            Fingerprint.BuildSignal("timezone", "Europe/Paris"),
            Fingerprint.BuildSignal("screen", "2560×1440 @ 24-bit"),
            Fingerprint.BuildSignal("user-agent", "Mozilla/5.0 (Macintosh)"),
        };

        foreach (var group in Fingerprint.GroupByCategory(signals))
        {
            Console.WriteLine($"[{group.Category.AsStr()}]");
            foreach (var signal in group.Signals)
            {
                Console.WriteLine($"  {signal.Label}: {signal.Value} ({signal.Risk.AsStr()} risk)");
            }
        }

        var counts = Fingerprint.CountByRisk(signals);
        Console.WriteLine($"risk mix: {counts.High} high / {counts.Medium} medium / {counts.Low} low");
        Console.WriteLine($"fingerprint: {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 →