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: C11 — standard library only (stdio / stdint / stdlib / string).
* 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: C has no standard cryptographic primitives. In production,
* hash with OpenSSL's libcrypto (EVP_Digest); 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 <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ------------------------------------------------------------------ */
/* Risk + category model */
/* ------------------------------------------------------------------ */
/* Privacy-risk classification. */
typedef enum {
RISK_LOW, /* Coarse; shared by millions of browsers. */
RISK_MEDIUM, /* Narrows you to a sizable-but-specific population. */
RISK_HIGH /* Near-unique on its own or part of a rare combination. */
} risk_level;
static const char *risk_level_str(risk_level risk)
{
switch (risk) {
case RISK_LOW:
return "low";
case RISK_MEDIUM:
return "medium";
case RISK_HIGH:
return "high";
}
return "?";
}
/* Groups related signals. */
typedef enum {
CATEGORY_HARDWARE,
CATEGORY_GRAPHICS,
CATEGORY_NETWORK,
CATEGORY_BROWSER,
CATEGORY_INPUT
} fingerprint_category;
static const char *category_str(fingerprint_category category)
{
switch (category) {
case CATEGORY_HARDWARE:
return "Hardware";
case CATEGORY_GRAPHICS:
return "Graphics";
case CATEGORY_NETWORK:
return "Network";
case CATEGORY_BROWSER:
return "Browser";
case CATEGORY_INPUT:
return "Input";
}
return "?";
}
/* Canonical category order (card grid, top to bottom). */
#define CATEGORY_COUNT 5
#define SIGNAL_COUNT 18
static const fingerprint_category FINGERPRINT_CATEGORIES[CATEGORY_COUNT] = {
CATEGORY_HARDWARE,
CATEGORY_GRAPHICS,
CATEGORY_NETWORK,
CATEGORY_BROWSER,
CATEGORY_INPUT,
};
/* One collected, classified signal. value is heap-owned; free it yourself. */
typedef struct {
const char *id;
const char *label;
char *value;
risk_level risk;
fingerprint_category category;
} fingerprint_signal;
/* Classifies one signal id. */
typedef struct {
const char *id;
const char *label;
risk_level risk;
fingerprint_category category;
} signal_def;
/* 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. */
static const signal_def SIGNAL_DEFINITIONS[SIGNAL_COUNT] = {
/* High risk */
{ "canvas", "Canvas fingerprint", RISK_HIGH, CATEGORY_GRAPHICS },
{ "webgl-renderer", "WebGL renderer", RISK_HIGH, CATEGORY_GRAPHICS },
{ "webgl-vendor", "WebGL vendor", RISK_HIGH, CATEGORY_GRAPHICS },
{ "fonts", "Installed fonts", RISK_HIGH, CATEGORY_BROWSER },
{ "timezone", "Timezone", RISK_HIGH, CATEGORY_BROWSER },
/* Medium risk */
{ "screen", "Screen & color depth", RISK_MEDIUM, CATEGORY_HARDWARE },
{ "device-pixel-ratio", "Device pixel ratio", RISK_MEDIUM, CATEGORY_HARDWARE },
{ "hardware-concurrency", "CPU cores", RISK_MEDIUM, CATEGORY_HARDWARE },
{ "device-memory", "Device Memory", RISK_MEDIUM, CATEGORY_HARDWARE },
{ "platform", "Platform", RISK_MEDIUM, CATEGORY_BROWSER },
{ "languages", "Languages", RISK_MEDIUM, CATEGORY_BROWSER },
{ "touch", "Touch support", RISK_MEDIUM, CATEGORY_INPUT },
{ "connection", "Connection type", RISK_MEDIUM, CATEGORY_NETWORK },
/* Low risk */
{ "user-agent", "User agent", RISK_LOW, CATEGORY_BROWSER },
{ "do-not-track", "Do Not Track", RISK_LOW, CATEGORY_BROWSER },
{ "cookies-enabled", "Cookies enabled", RISK_LOW, CATEGORY_BROWSER },
{ "online", "Online status", RISK_LOW, CATEGORY_NETWORK },
{ "pdf-viewer", "PDF viewer", RISK_LOW, CATEGORY_BROWSER },
};
static const signal_def *signal_def_find(const char *id)
{
for (size_t i = 0; i < SIGNAL_COUNT; i++) {
if (strcmp(SIGNAL_DEFINITIONS[i].id, id) == 0) {
return &SIGNAL_DEFINITIONS[i];
}
}
return NULL;
}
/* Risk level for a known signal id; NULL for unknown ids. */
static const risk_level *classify_signal_risk(const char *id)
{
const signal_def *def = signal_def_find(id);
return def == NULL ? NULL : &def->risk;
}
static char *dup_str(const char *src)
{
size_t len = strlen(src) + 1;
char *copy = malloc(len);
if (copy != NULL) {
memcpy(copy, src, len);
}
return copy;
}
/* Build a signal from its id and collected value. Returns 0 on success, -1
* on unknown id (so a typo'd id fails loudly instead of silently rendering
* an unclassified row) or allocation failure. out->value must be freed. */
static int build_signal(const char *id, const char *value, fingerprint_signal *out)
{
const signal_def *def = signal_def_find(id);
if (def == NULL) {
fprintf(stderr, "Unknown fingerprint signal id: %s\n", id);
return -1;
}
char *copy = dup_str(value);
if (copy == NULL) {
return -1;
}
out->id = def->id;
out->label = def->label;
out->value = copy;
out->risk = def->risk;
out->category = def->category;
return 0;
}
/* ------------------------------------------------------------------ */
/* Grouping + counting */
/* ------------------------------------------------------------------ */
/* One category and its signals (pointers into the caller's array). */
typedef struct {
fingerprint_category category;
const fingerprint_signal *signals[SIGNAL_COUNT];
size_t count;
} category_group;
/* Group signals by category in canonical order, omitting empty categories.
* groups_out must have room for CATEGORY_COUNT entries; returns the group
* count. */
static size_t group_by_category(const fingerprint_signal *signals, size_t signal_count,
category_group *groups_out)
{
size_t group_count = 0;
for (size_t c = 0; c < CATEGORY_COUNT; c++) {
fingerprint_category category = FINGERPRINT_CATEGORIES[c];
category_group *group = NULL;
for (size_t i = 0; i < signal_count; i++) {
if (signals[i].category != category) {
continue;
}
if (group == NULL) {
group = &groups_out[group_count++];
group->category = category;
group->count = 0;
}
group->signals[group->count++] = &signals[i];
}
}
return group_count;
}
/* Signals per risk level (drives the summary line). */
typedef struct {
size_t low;
size_t medium;
size_t high;
} risk_counts;
static risk_counts count_by_risk(const fingerprint_signal *signals, size_t signal_count)
{
risk_counts counts = { 0, 0, 0 };
for (size_t i = 0; i < signal_count; i++) {
switch (signals[i].risk) {
case RISK_LOW:
counts.low++;
break;
case RISK_MEDIUM:
counts.medium++;
break;
case RISK_HIGH:
counts.high++;
break;
}
}
return counts;
}
/* The exact byte string the fingerprint hash is computed over: every value
* joined with '|' in signal order. Caller frees. */
static char *concat_signal_values(const fingerprint_signal *signals, size_t signal_count)
{
size_t total = 1; /* NUL terminator */
for (size_t i = 0; i < signal_count; i++) {
total += strlen(signals[i].value);
if (i > 0) {
total += 1; /* separator */
}
}
char *joined = malloc(total);
if (joined == NULL) {
return NULL;
}
char *cursor = joined;
for (size_t i = 0; i < signal_count; i++) {
if (i > 0) {
*cursor++ = '|';
}
size_t len = strlen(signals[i].value);
memcpy(cursor, signals[i].value, len);
cursor += len;
}
*cursor = '\0';
return joined;
}
/* ------------------------------------------------------------------ */
/* SHA-256 (FIPS 180-4) — dependency-free, pinned by the "abc" vector */
/* ------------------------------------------------------------------ */
static uint32_t rotr32(uint32_t x, unsigned n)
{
return (x >> n) | (x << (32 - n));
}
/* Round constants: first 32 bits of the fractional parts of the cube roots
* of the first 64 primes. */
static const 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. */
static const uint32_t SHA256_H0[8] = {
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
};
/* Minimal SHA-256 state machine (single-shot digest over a full message). */
typedef struct {
uint32_t state[8];
} sha256;
/* Process one 64-byte block through the 64-round compression function. */
static void sha256_compress(sha256 *ctx, const uint8_t *block)
{
uint32_t w[64];
for (unsigned t = 0; t < 16; t++) {
w[t] = ((uint32_t)block[4 * t] << 24) | ((uint32_t)block[4 * t + 1] << 16) |
((uint32_t)block[4 * t + 2] << 8) | (uint32_t)block[4 * t + 3];
}
for (unsigned t = 16; t < 64; t++) {
uint32_t s0 = rotr32(w[t - 15], 7) ^ rotr32(w[t - 15], 18) ^ (w[t - 15] >> 3);
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;
}
uint32_t a = ctx->state[0], b = ctx->state[1], c = ctx->state[2], d = ctx->state[3];
uint32_t e = ctx->state[4], f = ctx->state[5], g = ctx->state[6], h = ctx->state[7];
for (unsigned t = 0; t < 64; t++) {
uint32_t big_s1 = rotr32(e, 6) ^ rotr32(e, 11) ^ rotr32(e, 25);
uint32_t ch = (e & f) ^ (~e & g);
uint32_t temp1 = h + big_s1 + ch + SHA256_K[t] + w[t];
uint32_t big_s0 = rotr32(a, 2) ^ rotr32(a, 13) ^ rotr32(a, 22);
uint32_t maj = (a & b) ^ (a & c) ^ (b & c);
uint32_t temp2 = big_s0 + maj;
h = g;
g = f;
f = e;
e = d + temp1;
d = c;
c = b;
b = a;
a = temp1 + temp2;
}
ctx->state[0] += a;
ctx->state[1] += b;
ctx->state[2] += c;
ctx->state[3] += d;
ctx->state[4] += e;
ctx->state[5] += f;
ctx->state[6] += g;
ctx->state[7] += h;
}
/* Pad (0x80, zeros, 64-bit big-endian bit length) and digest the whole
* message into out[32]. Returns 0 on success, -1 on allocation failure. */
static int sha256_digest(const uint8_t *message, size_t len, uint8_t out[32])
{
sha256 ctx;
memcpy(ctx.state, SHA256_H0, sizeof ctx.state);
uint64_t bit_len = (uint64_t)len * 8;
size_t padded_len = ((len + 9 + 63) / 64) * 64; /* message + 0x80 + length */
uint8_t *padded = calloc(padded_len, 1);
if (padded == NULL) {
return -1;
}
memcpy(padded, message, len);
padded[len] = 0x80;
for (unsigned i = 0; i < 8; i++) {
padded[padded_len - 1 - i] = (uint8_t)(bit_len >> (8 * i));
}
for (size_t offset = 0; offset < padded_len; offset += 64) {
sha256_compress(&ctx, padded + offset);
}
free(padded);
for (unsigned i = 0; i < 8; i++) {
out[4 * i] = (uint8_t)(ctx.state[i] >> 24);
out[4 * i + 1] = (uint8_t)(ctx.state[i] >> 16);
out[4 * i + 2] = (uint8_t)(ctx.state[i] >> 8);
out[4 * i + 3] = (uint8_t)ctx.state[i];
}
return 0;
}
/* SHA-256 hex digest of the bytes of input. Caller frees. */
static char *sha256_hex(const char *input)
{
uint8_t digest[32];
if (sha256_digest((const uint8_t *)input, strlen(input), digest) != 0) {
return NULL;
}
char *hex = malloc(65);
if (hex == NULL) {
return NULL;
}
static const char HEX[] = "0123456789abcdef";
for (unsigned i = 0; i < 32; i++) {
hex[2 * i] = HEX[digest[i] >> 4];
hex[2 * i + 1] = HEX[digest[i] & 0x0f];
}
hex[64] = '\0';
return hex;
}
/* 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 frees. */
static char *hash_fingerprint(const fingerprint_signal *signals, size_t signal_count)
{
char *joined = concat_signal_values(signals, signal_count);
if (joined == NULL) {
return NULL;
}
char *hex = sha256_hex(joined);
free(joined);
return hex;
}
int main(void)
{
/* FIPS 180-4 vector pins the hand-rolled implementation to the spec. */
char *abc = sha256_hex("abc");
if (abc == NULL ||
strcmp(abc, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad") != 0) {
fprintf(stderr, "SHA-256 self-check failed\n");
free(abc);
return EXIT_FAILURE;
}
free(abc);
/* The registry is the single source of truth: known ids classify,
* unknown ids do not. */
if (classify_signal_risk("canvas") == NULL || classify_signal_risk("no-such-signal") != NULL) {
fprintf(stderr, "registry self-check failed\n");
return EXIT_FAILURE;
}
fingerprint_signal signals[3];
if (build_signal("timezone", "Europe/Paris", &signals[0]) != 0 ||
build_signal("screen", "2560×1440 @ 24-bit", &signals[1]) != 0 ||
build_signal("user-agent", "Mozilla/5.0 (Macintosh)", &signals[2]) != 0) {
return EXIT_FAILURE;
}
category_group groups[CATEGORY_COUNT];
size_t group_count = group_by_category(signals, 3, groups);
for (size_t g = 0; g < group_count; g++) {
printf("[%s]\n", category_str(groups[g].category));
for (size_t i = 0; i < groups[g].count; i++) {
const fingerprint_signal *s = groups[g].signals[i];
printf(" %s: %s (%s risk)\n", s->label, s->value, risk_level_str(s->risk));
}
}
risk_counts counts = count_by_risk(signals, 3);
printf("risk mix: %zu high / %zu medium / %zu low\n", counts.high, counts.medium, counts.low);
char *fingerprint = hash_fingerprint(signals, 3);
if (fingerprint == NULL) {
return EXIT_FAILURE;
}
printf("fingerprint: %s\n", fingerprint);
free(fingerprint);
for (size_t i = 0; i < 3; i++) {
free(signals[i].value);
}
return EXIT_SUCCESS;
}
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 →