Skip to content

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 →