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: 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 →