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++17 — standard library only (string / vector / optional).
// 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.
//
// SHA-256 note: the C++ standard library ships no cryptographic primitives.
// In production, hash with OpenSSL's EVP interface (or the header-only
// picosha2); this snippet hand-rolls FIPS 180-4 SHA-256 only to stay
// dependency-free. The "abc" check in main() pins the implementation to the
// specification.

#include <array>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <optional>
#include <string>
#include <vector>

namespace browser_fingerprint {

// Privacy-risk classification.
enum class risk_level { low, medium, high };

inline const char *as_str(risk_level risk)
{
    switch (risk) {
    case risk_level::low:
        return "low";
    case risk_level::medium:
        return "medium";
    case risk_level::high:
        return "high";
    }
    return "?";
}

// Groups related signals.
enum class fingerprint_category { hardware, graphics, network, browser, input };

inline const char *as_str(fingerprint_category category)
{
    switch (category) {
    case fingerprint_category::hardware:
        return "Hardware";
    case fingerprint_category::graphics:
        return "Graphics";
    case fingerprint_category::network:
        return "Network";
    case fingerprint_category::browser:
        return "Browser";
    case fingerprint_category::input:
        return "Input";
    }
    return "?";
}

// Canonical category order (card grid, top to bottom).
inline constexpr fingerprint_category fingerprint_categories[5] = {
    fingerprint_category::hardware,
    fingerprint_category::graphics,
    fingerprint_category::network,
    fingerprint_category::browser,
    fingerprint_category::input,
};

// One collected, classified signal.
struct fingerprint_signal {
    std::string id;
    std::string label;
    std::string value;
    risk_level risk;
    fingerprint_category category;
};

// Classifies one signal id.
struct signal_def {
    const char *id;
    const char *label;
    risk_level risk;
    fingerprint_category category;
};

// Static registry of every signal the tool collects, with its privacy-risk
// classification. Single source of truth: build_signal() derives from it, so
// a signal can never be collected without being classified.
inline constexpr signal_def signal_definitions[18] = {
    // High risk
    { "canvas", "Canvas fingerprint", risk_level::high, fingerprint_category::graphics },
    { "webgl-renderer", "WebGL renderer", risk_level::high, fingerprint_category::graphics },
    { "webgl-vendor", "WebGL vendor", risk_level::high, fingerprint_category::graphics },
    { "fonts", "Installed fonts", risk_level::high, fingerprint_category::browser },
    { "timezone", "Timezone", risk_level::high, fingerprint_category::browser },
    // Medium risk
    { "screen", "Screen & color depth", risk_level::medium, fingerprint_category::hardware },
    { "device-pixel-ratio", "Device pixel ratio", risk_level::medium, fingerprint_category::hardware },
    { "hardware-concurrency", "CPU cores", risk_level::medium, fingerprint_category::hardware },
    { "device-memory", "Device Memory", risk_level::medium, fingerprint_category::hardware },
    { "platform", "Platform", risk_level::medium, fingerprint_category::browser },
    { "languages", "Languages", risk_level::medium, fingerprint_category::browser },
    { "touch", "Touch support", risk_level::medium, fingerprint_category::input },
    { "connection", "Connection type", risk_level::medium, fingerprint_category::network },
    // Low risk
    { "user-agent", "User agent", risk_level::low, fingerprint_category::browser },
    { "do-not-track", "Do Not Track", risk_level::low, fingerprint_category::browser },
    { "cookies-enabled", "Cookies enabled", risk_level::low, fingerprint_category::browser },
    { "online", "Online status", risk_level::low, fingerprint_category::network },
    { "pdf-viewer", "PDF viewer", risk_level::low, fingerprint_category::browser },
};

inline const signal_def *signal_def_find(const std::string &id)
{
    for (const signal_def &def : signal_definitions) {
        if (id == def.id) {
            return &def;
        }
    }
    return nullptr;
}

// Risk level for a known signal id; std::nullopt for unknown ids.
inline std::optional<risk_level> classify_signal_risk(const std::string &id)
{
    const signal_def *def = signal_def_find(id);
    if (def == nullptr) {
        return std::nullopt;
    }
    return def->risk;
}

// Build a signal from its id and collected value. Returns std::nullopt on
// unknown ids so a typo'd id fails loudly instead of silently rendering an
// unclassified row.
inline std::optional<fingerprint_signal> build_signal(const std::string &id, std::string value)
{
    const signal_def *def = signal_def_find(id);
    if (def == nullptr) {
        std::fprintf(stderr, "Unknown fingerprint signal id: %s\n", id.c_str());
        return std::nullopt;
    }
    return fingerprint_signal{ id, def->label, std::move(value), def->risk, def->category };
}

// One category and its signals.
struct category_group {
    fingerprint_category category;
    std::vector<fingerprint_signal> signals;
};

// Group signals by category in canonical order, omitting empty categories.
inline std::vector<category_group> group_by_category(const std::vector<fingerprint_signal> &signals)
{
    std::vector<category_group> groups;
    for (fingerprint_category category : fingerprint_categories) {
        category_group group{ category, {} };
        for (const fingerprint_signal &signal : signals) {
            if (signal.category == category) {
                group.signals.push_back(signal);
            }
        }
        if (!group.signals.empty()) {
            groups.push_back(std::move(group));
        }
    }
    return groups;
}

// Signals per risk level (drives the summary line).
struct risk_counts {
    std::size_t low = 0;
    std::size_t medium = 0;
    std::size_t high = 0;
};

// Count signals per risk level.
inline risk_counts count_by_risk(const std::vector<fingerprint_signal> &signals)
{
    risk_counts counts;
    for (const fingerprint_signal &signal : signals) {
        switch (signal.risk) {
        case risk_level::low:
            counts.low++;
            break;
        case risk_level::medium:
            counts.medium++;
            break;
        case risk_level::high:
            counts.high++;
            break;
        }
    }
    return counts;
}

// The exact byte string the fingerprint hash is computed over: every value
// joined with '|' in signal order.
inline std::string concat_signal_values(const std::vector<fingerprint_signal> &signals)
{
    std::string joined;
    for (std::size_t i = 0; i < signals.size(); i++) {
        if (i > 0) {
            joined += '|';
        }
        joined += signals[i].value;
    }
    return joined;
}

// ---------------------------------------------------------------------------
// SHA-256 (FIPS 180-4) — dependency-free, pinned by the "abc" vector
// ---------------------------------------------------------------------------

namespace detail {

// Round constants: first 32 bits of the fractional parts of the cube roots
// of the first 64 primes.
inline constexpr std::uint32_t sha256_k[64] = {
    0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
    0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
    0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
    0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
    0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
    0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
    0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
    0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
};

// Initial hash words: first 32 bits of the fractional parts of the square
// roots of the first 8 primes.
inline constexpr std::uint32_t sha256_h0[8] = {
    0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
};

inline constexpr std::uint32_t rotr32(std::uint32_t x, unsigned n)
{
    return (x >> n) | (x << (32 - n));
}

// Minimal SHA-256 state machine (single-shot digest over a full message).
struct sha256 {
    std::uint32_t state[8];

    sha256() { std::memcpy(state, sha256_h0, sizeof state); }

    // Process one 64-byte block through the 64-round compression function.
    void compress(const std::uint8_t *block)
    {
        std::uint32_t w[64];
        for (unsigned t = 0; t < 16; t++) {
            w[t] = (static_cast<std::uint32_t>(block[4 * t]) << 24) |
                   (static_cast<std::uint32_t>(block[4 * t + 1]) << 16) |
                   (static_cast<std::uint32_t>(block[4 * t + 2]) << 8) |
                   static_cast<std::uint32_t>(block[4 * t + 3]);
        }
        for (unsigned t = 16; t < 64; t++) {
            std::uint32_t s0 = rotr32(w[t - 15], 7) ^ rotr32(w[t - 15], 18) ^ (w[t - 15] >> 3);
            std::uint32_t s1 = rotr32(w[t - 2], 17) ^ rotr32(w[t - 2], 19) ^ (w[t - 2] >> 10);
            w[t] = w[t - 16] + s0 + w[t - 7] + s1;
        }

        std::uint32_t a = state[0], b = state[1], c = state[2], d = state[3];
        std::uint32_t e = state[4], f = state[5], g = state[6], h = state[7];
        for (unsigned t = 0; t < 64; t++) {
            std::uint32_t big_s1 = rotr32(e, 6) ^ rotr32(e, 11) ^ rotr32(e, 25);
            std::uint32_t ch = (e & f) ^ (~e & g);
            std::uint32_t temp1 = h + big_s1 + ch + sha256_k[t] + w[t];
            std::uint32_t big_s0 = rotr32(a, 2) ^ rotr32(a, 13) ^ rotr32(a, 22);
            std::uint32_t maj = (a & b) ^ (a & c) ^ (b & c);
            std::uint32_t temp2 = big_s0 + maj;

            h = g;
            g = f;
            f = e;
            e = d + temp1;
            d = c;
            c = b;
            b = a;
            a = temp1 + temp2;
        }

        state[0] += a;
        state[1] += b;
        state[2] += c;
        state[3] += d;
        state[4] += e;
        state[5] += f;
        state[6] += g;
        state[7] += h;
    }

    // Pad (0x80, zeros, 64-bit big-endian bit length) and digest the whole
    // message, returning the 32-byte hash.
    std::array<std::uint8_t, 32> digest(const std::string &message)
    {
        const std::uint64_t bit_len = static_cast<std::uint64_t>(message.size()) * 8;
        std::vector<std::uint8_t> padded(message.begin(), message.end());
        padded.push_back(0x80);
        while (padded.size() % 64 != 56) {
            padded.push_back(0);
        }
        for (unsigned i = 0; i < 8; i++) {
            padded.push_back(static_cast<std::uint8_t>(bit_len >> (56 - 8 * i)));
        }

        for (std::size_t offset = 0; offset < padded.size(); offset += 64) {
            compress(padded.data() + offset);
        }

        std::array<std::uint8_t, 32> out{};
        for (unsigned i = 0; i < 8; i++) {
            out[4 * i] = static_cast<std::uint8_t>(state[i] >> 24);
            out[4 * i + 1] = static_cast<std::uint8_t>(state[i] >> 16);
            out[4 * i + 2] = static_cast<std::uint8_t>(state[i] >> 8);
            out[4 * i + 3] = static_cast<std::uint8_t>(state[i]);
        }
        return out;
    }
};

} // namespace detail

// SHA-256 hex digest of the UTF-8 bytes of input.
inline std::string sha256_hex(const std::string &input)
{
    const auto digest = detail::sha256{}.digest(input);
    static const char hex[] = "0123456789abcdef";
    std::string out;
    out.reserve(64);
    for (std::uint8_t byte : digest) {
        out += hex[byte >> 4];
        out += hex[byte & 0x0f];
    }
    return out;
}

// Stable fingerprint ID: SHA-256 over every signal value, joined in signal
// order. Same browser state -> same hash; any single changed value -> new
// hash.
inline std::string hash_fingerprint(const std::vector<fingerprint_signal> &signals)
{
    return sha256_hex(concat_signal_values(signals));
}

} // namespace browser_fingerprint

int main()
{
    using namespace browser_fingerprint;

    // FIPS 180-4 vector pins the hand-rolled implementation to the spec.
    if (sha256_hex("abc") != "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad") {
        std::fprintf(stderr, "SHA-256 self-check failed\n");
        return 1;
    }
    // The registry is the single source of truth: known ids classify,
    // unknown ids do not.
    if (!classify_signal_risk("canvas").has_value() || classify_signal_risk("no-such-signal").has_value()) {
        std::fprintf(stderr, "registry self-check failed\n");
        return 1;
    }

    // Unknown ids are rejected loudly: .value() throws std::bad_optional_access
    // here instead of silently rendering an unclassified row.
    std::vector<fingerprint_signal> signals = {
        build_signal("timezone", "Europe/Paris").value(),
        build_signal("screen", "2560×1440 @ 24-bit").value(),
        build_signal("user-agent", "Mozilla/5.0 (Macintosh)").value(),
    };

    for (const category_group &group : group_by_category(signals)) {
        std::printf("[%s]\n", as_str(group.category));
        for (const fingerprint_signal &signal : group.signals) {
            std::printf("  %s: %s (%s risk)\n", signal.label.c_str(), signal.value.c_str(),
                        as_str(signal.risk));
        }
    }

    const risk_counts counts = count_by_risk(signals);
    std::printf("risk mix: %zu high / %zu medium / %zu low\n", counts.high, counts.medium, counts.low);
    std::printf("fingerprint: %s\n", hash_fingerprint(signals).c_str());
    return 0;
}

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 →