Browser Fingerprint Viewer — Zig 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 Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Browser Fingerprint — pure logic (Zig port): the 18-signal privacy-risk
// registry with canonical grouping, risk counts, and a stable SHA-256
// fingerprint hash.
//
// Language: Zig 0.13 — standard library only (std.crypto.hash.sha2 for SHA-256).
// 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.
const std = @import("std");
/// Privacy-risk classification.
pub const RiskLevel = enum {
/// Coarse; shared by millions of browsers.
low,
/// Narrows you to a sizable-but-specific population.
medium,
/// Near-unique on its own or part of a rare combination.
high,
pub fn str(self: RiskLevel) []const u8 {
return switch (self) {
RiskLevel.low => "low",
RiskLevel.medium => "medium",
RiskLevel.high => "high",
};
}
};
/// Groups related signals.
pub const FingerprintCategory = enum {
hardware,
graphics,
network,
browser,
input,
pub fn str(self: FingerprintCategory) []const u8 {
return switch (self) {
FingerprintCategory.hardware => "Hardware",
FingerprintCategory.graphics => "Graphics",
FingerprintCategory.network => "Network",
FingerprintCategory.browser => "Browser",
FingerprintCategory.input => "Input",
};
}
};
/// Canonical category order (card grid, top to bottom).
pub const fingerprint_categories = [_]FingerprintCategory{
.hardware, .graphics, .network, .browser, .input,
};
/// One collected, classified signal.
pub const FingerprintSignal = struct {
id: []const u8,
label: []const u8,
value: []const u8,
risk: RiskLevel,
category: FingerprintCategory,
};
/// Classifies one signal id.
const SignalDef = struct {
label: []const u8,
risk: RiskLevel,
category: FingerprintCategory,
};
const SignalEntry = struct {
id: []const u8,
def: SignalDef,
};
/// 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.
const signal_definitions = [_]SignalEntry{
// High risk
.{ .id = "canvas", .def = .{ .label = "Canvas fingerprint", .risk = .high, .category = .graphics } },
.{ .id = "webgl-renderer", .def = .{ .label = "WebGL renderer", .risk = .high, .category = .graphics } },
.{ .id = "webgl-vendor", .def = .{ .label = "WebGL vendor", .risk = .high, .category = .graphics } },
.{ .id = "fonts", .def = .{ .label = "Installed fonts", .risk = .high, .category = .browser } },
.{ .id = "timezone", .def = .{ .label = "Timezone", .risk = .high, .category = .browser } },
// Medium risk
.{ .id = "screen", .def = .{ .label = "Screen & color depth", .risk = .medium, .category = .hardware } },
.{ .id = "device-pixel-ratio", .def = .{ .label = "Device pixel ratio", .risk = .medium, .category = .hardware } },
.{ .id = "hardware-concurrency", .def = .{ .label = "CPU cores", .risk = .medium, .category = .hardware } },
.{ .id = "device-memory", .def = .{ .label = "Device Memory", .risk = .medium, .category = .hardware } },
.{ .id = "platform", .def = .{ .label = "Platform", .risk = .medium, .category = .browser } },
.{ .id = "languages", .def = .{ .label = "Languages", .risk = .medium, .category = .browser } },
.{ .id = "touch", .def = .{ .label = "Touch support", .risk = .medium, .category = .input } },
.{ .id = "connection", .def = .{ .label = "Connection type", .risk = .medium, .category = .network } },
// Low risk
.{ .id = "user-agent", .def = .{ .label = "User agent", .risk = .low, .category = .browser } },
.{ .id = "do-not-track", .def = .{ .label = "Do Not Track", .risk = .low, .category = .browser } },
.{ .id = "cookies-enabled", .def = .{ .label = "Cookies enabled", .risk = .low, .category = .browser } },
.{ .id = "online", .def = .{ .label = "Online status", .risk = .low, .category = .network } },
.{ .id = "pdf-viewer", .def = .{ .label = "PDF viewer", .risk = .low, .category = .browser } },
};
fn signalDef(id: []const u8) ?*const SignalDef {
for (&signal_definitions) |*entry| {
if (std.mem.eql(u8, entry.id, id)) {
return &entry.def;
}
}
return null;
}
/// Risk level for a known signal id; null for unknown ids.
pub fn classifySignalRisk(id: []const u8) ?RiskLevel {
const def = signalDef(id) orelse return null;
return def.risk;
}
pub const FingerprintError = error{UnknownSignal};
/// Build a signal from its id and collected value. Returns
/// error.UnknownSignal on unknown ids so a typo'd id fails loudly instead of
/// silently rendering an unclassified row.
pub fn buildSignal(id: []const u8, value: []const u8) FingerprintError!FingerprintSignal {
const def = signalDef(id) orelse return error.UnknownSignal;
return .{
.id = id,
.label = def.label,
.value = value,
.risk = def.risk,
.category = def.category,
};
}
/// One category and its signals.
pub const CategoryGroup = struct {
category: FingerprintCategory,
signals: std.ArrayList(FingerprintSignal),
};
/// Group signals by category in canonical order, omitting empty categories.
/// Caller owns the returned list and its members.
pub fn groupByCategory(allocator: std.mem.Allocator, signals: []const FingerprintSignal) !std.ArrayList(CategoryGroup) {
var groups = std.ArrayList(CategoryGroup).init(allocator);
for (fingerprint_categories) |category| {
var members = std.ArrayList(FingerprintSignal).init(allocator);
for (signals) |signal| {
if (signal.category == category) {
try members.append(signal);
}
}
if (members.items.len > 0) {
try groups.append(.{ .category = category, .signals = members });
} else {
members.deinit();
}
}
return groups;
}
/// Signals per risk level (drives the summary line).
pub const RiskCounts = struct {
low: usize = 0,
medium: usize = 0,
high: usize = 0,
};
/// Count signals per risk level.
pub fn countByRisk(signals: []const FingerprintSignal) RiskCounts {
var counts = RiskCounts{};
for (signals) |signal| {
switch (signal.risk) {
.low => counts.low += 1,
.medium => counts.medium += 1,
.high => counts.high += 1,
}
}
return counts;
}
/// The exact byte string the fingerprint hash is computed over: every value
/// joined with '|' in signal order. Caller owns the returned slice.
pub fn concatSignalValues(allocator: std.mem.Allocator, signals: []const FingerprintSignal) ![]u8 {
const values = try allocator.alloc([]const u8, signals.len);
defer allocator.free(values);
for (signals, 0..) |signal, i| {
values[i] = signal.value;
}
return std.mem.join(allocator, "|", values);
}
/// SHA-256 hex digest of the UTF-8 bytes of input. Caller owns the returned
/// slice.
pub fn sha256Hex(allocator: std.mem.Allocator, input: []const u8) ![]u8 {
var digest: [32]u8 = undefined;
std.crypto.hash.sha2.Sha256.hash(input, &digest, .{});
return std.fmt.allocPrint(allocator, "{x}", .{&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. Caller owns the returned slice.
pub fn hashFingerprint(allocator: std.mem.Allocator, signals: []const FingerprintSignal) ![]u8 {
const joined = try concatSignalValues(allocator, signals);
defer allocator.free(joined);
return sha256Hex(allocator, joined);
}
pub fn main() !void {
// Arena allocator: one deinit frees every group, list, and string below.
var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena_state.deinit();
const allocator = arena_state.allocator();
const stdout = std.io.getStdOut().writer();
const signals = [_]FingerprintSignal{
try buildSignal("timezone", "Europe/Paris"),
try buildSignal("screen", "2560×1440 @ 24-bit"),
try buildSignal("user-agent", "Mozilla/5.0 (Macintosh)"),
};
const groups = try groupByCategory(allocator, &signals);
for (groups.items) |group| {
try stdout.print("[{s}]\n", .{group.category.str()});
for (group.signals.items) |signal| {
try stdout.print(" {s}: {s} ({s} risk)\n", .{ signal.label, signal.value, signal.risk.str() });
}
}
const counts = countByRisk(&signals);
try stdout.print("risk mix: {d} high / {d} medium / {d} low\n", .{ counts.high, counts.medium, counts.low });
const fingerprint = try hashFingerprint(allocator, &signals);
try stdout.print("fingerprint: {s}\n", .{fingerprint});
}
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 →