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 →