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 →