Skip to content

Passphrase Generator — C source

Generate memorable, high-entropy passphrases from the EFF diceware wordlist using cryptographic randomness. Each word is drawn with crypto.getRandomValues and rejection sampling (no modulo bias), and every passphrase shows its exact entropy in bits plus the estimated crack time at 1 trillion guesses/sec. Everything runs locally - nothing is sent anywhere.

This is the C implementation — the same logic the interactive tool runs, in a shareable, citable form.

/*
 * passphrase-generator — diceware passphrase generation, entropy scoring and
 *                        crack-time estimation over the EFF long wordlist.
 *
 * Language: C (C11, standard library + OpenSSL 3.x libcrypto for the CSPRNG —
 *           C's rand() is not cryptographically secure and must never pick
 *           passphrase words)
 * Source:   CosmoDev polyglot showcase port of the Passphrase Generator tool,
 *           ported from src/lib/passphrase-generator.ts (the canonical
 *           TypeScript implementation).
 * License:  display source — part of CosmoDev's polyglot tool pages.
 *
 * NOTE ON THE WORDLIST: the 7,776-word EFF long wordlist is NOT embedded here —
 * it must be loaded separately at runtime. The TS reference imports it from
 * src/lib/eff-wordlist.ts; this port reads the official file (eff_large_word-
 * list.txt, "roll<TAB>word" per line, from eff.org/dice) via load_eff_wordlist().
 * Keeping it out of the source keeps this snippet readable and the list
 * canonical — a truncated or re-typed list silently destroys the entropy
 * guarantee this tool exists to provide.
 *
 * Word selection uses the CSPRNG with rejection sampling, so every word index
 * is uniform over the 7,776 entries — a plain `% 7776` on a uint16 would favour
 * the first 65536 % 7776 = 2048 words and quietly cost ~0.05 bits per word.
 *
 * Build: cc -std=c11 passphrase-generator.c -lcrypto
 */

#include <math.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <openssl/rand.h>

/* --------------------------------------------------------------- constants --- */

/* Size of the EFF long wordlist (6^5 — five dice rolls per word). */
#define EFF_WORDLIST_LENGTH 7776

enum {
    MIN_WORDS     = 3,
    MAX_WORDS     = 10,
    DEFAULT_WORDS = 6,
    MAX_WORD_LEN  = 32
};

static const double DEFAULT_GUESSES_PER_SECOND = 1e12;

/* Rejection-sampling ceiling for a uniform uint16 over 7,776: the largest
 * multiple of 7,776 that fits in [0, 65536). 7776 * 8 = 62208; draws >= 62208
 * are rejected so `v % 7776` is uniform. */
#define WORD_LIMIT (EFF_WORDLIST_LENGTH * (65536 / EFF_WORDLIST_LENGTH))

typedef enum {
    SEP_SPACE,
    SEP_DASH,
    SEP_DOT,
    SEP_UNDERSCORE,
    SEP_NONE
} separator;

static const char *separator_text(separator s)
{
    switch (s) {
        case SEP_DASH:       return "-";
        case SEP_DOT:        return ".";
        case SEP_UNDERSCORE: return "_";
        case SEP_NONE:       return "";
        case SEP_SPACE:
        default:             return " ";
    }
}

typedef struct {
    int       word_count;
    separator sep;
    bool      capitalize;
    bool      append_digit;
    /* Attack speed for the crack-time estimate. 0 = the 1 trillion/s default. */
    double    guesses_per_second;
} passphrase_options;

typedef struct {
    double seconds;
    char   human[64];
} crack_time_estimate;

typedef struct {
    char                words[MAX_WORDS][MAX_WORD_LEN];
    int                 word_count;
    char                passphrase[MAX_WORDS * (MAX_WORD_LEN + 1) + 2];
    double              entropy;
    crack_time_estimate crack_time;
} passphrase;

/* ---------------------------------------------------------------- wordlist --- */

/* The loaded wordlist. `count` is 0 until load_eff_wordlist() succeeds. */
typedef struct {
    char (*words)[MAX_WORD_LEN];
    size_t count;
} eff_wordlist;

/*
 * Load the official EFF long wordlist from disk. The file has one
 * "11111<TAB>abacus" entry per line; only the word half is kept. Returns false
 * unless exactly EFF_WORDLIST_LENGTH words were read — a short list would
 * silently lower the entropy of every passphrase generated from it.
 */
bool load_eff_wordlist(const char *path, eff_wordlist *out)
{
    FILE *fp;
    char line[128];
    size_t n = 0;

    out->words = NULL;
    out->count = 0;

    fp = fopen(path, "r");
    if (fp == NULL) {
        return false;
    }
    out->words = malloc(EFF_WORDLIST_LENGTH * sizeof *out->words);
    if (out->words == NULL) {
        fclose(fp);
        return false;
    }

    while (n < EFF_WORDLIST_LENGTH && fgets(line, sizeof line, fp) != NULL) {
        /* Take the text after the tab, or the whole line if there is none. */
        char *word = strchr(line, '\t');
        char *end;

        word = (word != NULL) ? word + 1 : line;
        end = word + strcspn(word, " \t\r\n");
        *end = '\0';

        if (word[0] == '\0') {
            continue; /* blank line */
        }
        snprintf(out->words[n], MAX_WORD_LEN, "%s", word);
        n++;
    }
    fclose(fp);

    out->count = n;
    if (n != EFF_WORDLIST_LENGTH) {
        free(out->words);
        out->words = NULL;
        out->count = 0;
        return false;
    }
    return true;
}

void free_eff_wordlist(eff_wordlist *list)
{
    free(list->words);
    list->words = NULL;
    list->count = 0;
}

/* ------------------------------------------------------------------ random --- */

/* Draw one uniformly-random word index from the wordlist (CSPRNG). */
static bool random_word_index(uint16_t *out)
{
    uint16_t v;

    do {
        if (RAND_bytes((unsigned char *) &v, sizeof v) != 1) {
            return false;
        }
    } while (v >= WORD_LIMIT);

    *out = (uint16_t) (v % EFF_WORDLIST_LENGTH);
    return true;
}

/* Draw one uniformly-random digit 0-9 (CSPRNG, rejection-sampled on uint8). */
static bool random_digit(char *out)
{
    unsigned char v;

    do {
        if (RAND_bytes(&v, 1) != 1) {
            return false;
        }
    } while (v >= 250); /* largest multiple of 10 in [0, 256) */

    *out = (char) ('0' + (v % 10));
    return true;
}

/* ----------------------------------------------------------------- entropy --- */

/* Theoretical word entropy in bits: word_count * log2(7776). */
double entropy_bits(int word_count)
{
    if (word_count <= 0) {
        return 0.0;
    }
    return word_count * log2((double) EFF_WORDLIST_LENGTH);
}

/* Average time to crack (seconds) = 2^(bits-1) / guesses_per_second. */
double crack_time_seconds(double bits, double guesses_per_second)
{
    return pow(2.0, bits - 1.0) / guesses_per_second;
}

/* --------------------------------------------------------- time formatting --- */

#define YEAR_SECONDS 31557600.0 /* Julian year */

typedef struct {
    double      factor;
    const char *name;
} scale;

static const scale SCALES[] = {
    { 1e12, "trillion" },
    { 1e9,  "billion"  },
    { 1e6,  "million"  },
    { 1e3,  "thousand" },
};

/* One decimal below 10, whole numbers at or above it. */
static double round_span(double v)
{
    return (v >= 10.0) ? round(v) : round(v * 10.0) / 10.0;
}

/* Render a number with thousands separators, mirroring toLocaleString().
 * round_span() only ever leaves a fraction below 10, so grouping and the
 * single decimal never collide. */
static void format_number(double v, char *out, size_t out_size)
{
    char digits[64];
    char grouped[96];
    size_t len, g = 0;
    bool fractional = (v < 10.0) && (v != floor(v));

    if (fractional) {
        snprintf(out, out_size, "%.1f", v);
        return;
    }

    snprintf(digits, sizeof digits, "%.0f", v);
    len = strlen(digits);

    for (size_t i = 0; i < len && g + 2 < sizeof grouped; i++) {
        if (i > 0 && (len - i) % 3 == 0) {
            grouped[g++] = ',';
        }
        grouped[g++] = digits[i];
    }
    grouped[g] = '\0';
    snprintf(out, out_size, "%s", grouped);
}

/* Round + pluralise "<v> <unit>". */
static void span(double v, const char *singular, const char *plural,
                 char *out, size_t out_size)
{
    double n = round_span(v);
    char number[64];

    format_number(n, number, sizeof number);
    snprintf(out, out_size, "%s %s", number, (n == 1.0) ? singular : plural);
}

/*
 * Human-readable crack-time span. Uses centuries/millennia past a year, then
 * collapses to scaled words ("2.4 billion centuries") so the string stays
 * readable at diceware entropies (60-130 bits).
 */
void format_crack_time(double seconds, char *out, size_t out_size)
{
    double years, centuries;

    if (!isfinite(seconds) || seconds < 0.0) {
        snprintf(out, out_size, "-");
        return;
    }
    if (seconds < 1.0)     { snprintf(out, out_size, "< 1 second"); return; }
    if (seconds < 60.0)    { span(seconds, "second", "seconds", out, out_size); return; }
    if (seconds < 3600.0)  { span(seconds / 60.0, "minute", "minutes", out, out_size); return; }
    if (seconds < 86400.0) { span(seconds / 3600.0, "hour", "hours", out, out_size); return; }
    if (seconds < YEAR_SECONDS) { span(seconds / 86400.0, "day", "days", out, out_size); return; }

    years = seconds / YEAR_SECONDS;
    if (years < 100.0)  { span(years, "year", "years", out, out_size); return; }
    if (years < 1000.0) { span(years / 100.0, "century", "centuries", out, out_size); return; }
    if (years < 1e6)    { span(years / 1000.0, "millennium", "millennia", out, out_size); return; }

    /* years >= 1e6 here, so centuries >= 10,000: collapse to a scaled plural.
     * The 1e3 entry always matches, so the loop cannot fall through. */
    centuries = years / 100.0;
    for (size_t i = 0; i < sizeof SCALES / sizeof SCALES[0]; i++) {
        if (centuries >= SCALES[i].factor) {
            char number[64];
            format_number(round_span(centuries / SCALES[i].factor), number, sizeof number);
            snprintf(out, out_size, "%s %s centuries", number, SCALES[i].name);
            return;
        }
    }
}

/* ---------------------------------------------------------------- generate --- */

static int clamp_int(int v, int lo, int hi)
{
    return (v < lo) ? lo : (v > hi) ? hi : v;
}

/* Generate a diceware passphrase from the EFF long wordlist. */
bool generate_passphrase(const eff_wordlist *list,
                         const passphrase_options *options,
                         passphrase *out)
{
    passphrase_options opts = { DEFAULT_WORDS, SEP_SPACE, false, false, 0.0 };
    const char *sep;
    double gps;
    size_t written = 0;

    if (list == NULL || list->count != EFF_WORDLIST_LENGTH || out == NULL) {
        return false;
    }
    if (options != NULL) {
        opts = *options;
    }

    memset(out, 0, sizeof *out);
    out->word_count = clamp_int(opts.word_count, MIN_WORDS, MAX_WORDS);
    sep = separator_text(opts.sep);
    gps = (opts.guesses_per_second > 0.0) ? opts.guesses_per_second
                                          : DEFAULT_GUESSES_PER_SECOND;

    for (int i = 0; i < out->word_count; i++) {
        uint16_t index;
        char shown[MAX_WORD_LEN];
        int n;

        if (!random_word_index(&index)) {
            return false;
        }
        snprintf(out->words[i], MAX_WORD_LEN, "%s", list->words[index]);

        /* Capitalisation is a display transform only: `words` keeps the raw
         * draw, so the entropy accounting stays honest (case adds no entropy
         * because it is applied deterministically). */
        snprintf(shown, sizeof shown, "%s", out->words[i]);
        if (opts.capitalize && shown[0] >= 'a' && shown[0] <= 'z') {
            shown[0] = (char) (shown[0] - 'a' + 'A');
        }

        n = snprintf(out->passphrase + written, sizeof out->passphrase - written,
                     "%s%s", (i > 0) ? sep : "", shown);
        if (n < 0) {
            return false;
        }
        written += (size_t) n;
    }

    if (opts.append_digit && written + 1 < sizeof out->passphrase) {
        char digit;
        if (!random_digit(&digit)) {
            return false;
        }
        out->passphrase[written++] = digit;
        out->passphrase[written] = '\0';
    }

    out->entropy = entropy_bits(out->word_count);
    out->crack_time.seconds = crack_time_seconds(out->entropy, gps);
    format_crack_time(out->crack_time.seconds,
                      out->crack_time.human, sizeof out->crack_time.human);
    return true;
}

/* -------------------------------------------------------------------- demo --- */

int main(int argc, char **argv)
{
    /* The wordlist lives outside this snippet — see the header note. Download
     * it from https://www.eff.org/files/2016/07/18/eff_large_wordlist.txt */
    const char *path = (argc > 1) ? argv[1] : "eff_large_wordlist.txt";
    eff_wordlist list;
    passphrase_options options = { 6, SEP_DASH, true, true, 0.0 };
    passphrase result;

    if (!load_eff_wordlist(path, &list)) {
        fprintf(stderr,
                "Could not load a complete %d-word EFF list from \"%s\".\n"
                "Download it from https://www.eff.org/files/2016/07/18/eff_large_wordlist.txt\n"
                "and pass its path as the first argument.\n",
                EFF_WORDLIST_LENGTH, path);
        return EXIT_FAILURE;
    }

    if (!generate_passphrase(&list, &options, &result)) {
        fprintf(stderr, "Generation failed (no secure random source?).\n");
        free_eff_wordlist(&list);
        return EXIT_FAILURE;
    }

    printf("passphrase: %s\n", result.passphrase);
    printf("words:      %d\n", result.word_count);
    printf("entropy:    %.1f bits\n", result.entropy);
    printf("crack time: %s (at 1e12 guesses/s)\n", result.crack_time.human);

    /* The estimate scales with the word count, not with the words drawn. */
    for (int words = MIN_WORDS; words <= MAX_WORDS; words++) {
        double bits = entropy_bits(words);
        char human[64];

        format_crack_time(crack_time_seconds(bits, DEFAULT_GUESSES_PER_SECOND),
                          human, sizeof human);
        printf("  %2d words -> %5.1f bits -> %s\n", words, bits, human);
    }

    free_eff_wordlist(&list);
    return EXIT_SUCCESS;
}

Also available in 12 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 →