Skip to content

Passphrase Generator — Zig 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 Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.

//! passphrase-generator — diceware passphrase generation + entropy scoring.
//!
//! Language: Zig 0.14 (standard library only)
//! Ported from: src/lib/passphrase-generator.ts (the canonical TypeScript implementation).
//! display source — part of CosmoDev's polyglot tool pages.
//!
//! Word selection uses the Zig CSPRNG (std.crypto.random) with rejection
//! sampling, so every word index is uniform over the 7,776-word EFF long
//! wordlist — no modulo bias.
//!
//! NOTE: The EFF long wordlist (7,776 entries) is ~80 KB of data and is NOT
//! embedded here — the caller loads it separately and passes it as a slice
//! (the TS reference imports it from ./eff-wordlist.ts the same way).

const std = @import("std");

/// Size of the EFF long wordlist (6^5).
pub const eff_wordlist_length: usize = 7776;

pub const min_words: usize = 3;
pub const max_words: usize = 10;
pub const default_words: usize = 6;
pub const default_guesses_per_second: f64 = 1e12;

const year_seconds: f64 = 31_557_600; // Julian year

pub const Separator = enum {
    space,
    dash,
    dot,
    underscore,
    none,

    fn str(self: Separator) []const u8 {
        return switch (self) {
            .space => " ",
            .dash => "-",
            .dot => ".",
            .underscore => "_",
            .none => "",
        };
    }
};

pub const PassphraseOptions = struct {
    word_count: usize = default_words,
    separator: Separator = .space,
    capitalize: bool = false,
    append_digit: bool = false,
    /// Attack speed for the crack-time estimate. Default 1 trillion guesses/s.
    guesses_per_second: f64 = default_guesses_per_second,
};

pub const Passphrase = struct {
    words: [][]const u8,
    passphrase: []const u8,
    entropy: f64,
    crack_time_seconds: f64,
    crack_time_human: []const u8,
};

/// Rejection-sampling ceiling for a uniform u16 over 7,776: the largest
/// multiple of 7,776 that fits in [0, 65536). 7776 x 8 = 62208; draws >=
/// 62208 are rejected so `v % 7776` is uniform (a plain `% 7776` would favor
/// the first 65536 % 7776 = 2048 indexes).
const word_limit: u32 = 62208;

/// Draw one uniformly-random word from the EFF long wordlist (CSPRNG).
fn randomWord(wordlist: []const []const u8) []const u8 {
    var v: u32 = undefined;
    while (true) {
        v = std.crypto.random.int(u16);
        if (v < word_limit) break;
    }
    return wordlist[v % eff_wordlist_length];
}

/// Draw one uniformly-random digit 0-9 (CSPRNG, rejection-sampled on u8).
fn randomDigit() u8 {
    var v: u8 = undefined;
    while (true) {
        v = std.crypto.random.int(u8);
        if (v < 250) break; // largest multiple of 10 in [0, 256)
    }
    return '0' + (v % 10);
}

/// Theoretical word entropy in bits: wordCount x log2(7776).
pub fn entropyBits(word_count: usize) f64 {
    if (word_count == 0) return 0;
    return @as(f64, @floatFromInt(word_count)) * std.math.log2(
        @as(f64, @floatFromInt(eff_wordlist_length)),
    );
}

/// Average time to crack (seconds) = 2^(bits-1) / guessesPerSecond.
pub fn crackTimeSeconds(bits: f64, guesses_per_second: f64) f64 {
    return std.math.pow(f64, 2, bits - 1) / guesses_per_second;
}

/// Round + pluralize `v unit`; 1 decimal below 10, whole numbers above.
/// `buf` receives the formatted number and the singular/plural unit name.
fn span(buf: []u8, v: f64, singular: []const u8, plural: []const u8) []const u8 {
    const n = round(v);
    const unit = if (n == 1.0) singular else plural;
    if (n == @trunc(n) or n >= 10.0) {
        return std.fmt.bufPrint(buf, "{d:.0} {s}", .{ n, unit }) catch unreachable;
    }
    return std.fmt.bufPrint(buf, "{d:.1} {s}", .{ n, unit }) catch unreachable;
}

fn round(v: f64) f64 {
    if (v >= 10.0) return @round(v);
    return @round(v * 10) / 10;
}

const Scale = struct { factor: f64, name: []const u8 };
const scales = [_]Scale{
    .{ .factor = 1e12, .name = "trillion" },
    .{ .factor = 1e9, .name = "billion" },
    .{ .factor = 1e6, .name = "million" },
    .{ .factor = 1e3, .name = "thousand" },
};

/// 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).
pub fn formatCrackTime(buf: []u8, seconds: f64) []const u8 {
    if (!std.math.isFinite(seconds) or seconds < 0) return "-";
    if (seconds < 1) return "< 1 second";
    if (seconds < 60) return span(buf, seconds, "second", "seconds");
    if (seconds < 3600) return span(buf, seconds / 60, "minute", "minutes");
    if (seconds < 86_400) return span(buf, seconds / 3600, "hour", "hours");
    if (seconds < year_seconds) return span(buf, seconds / 86_400, "day", "days");

    const years = seconds / year_seconds;
    if (years < 100) return span(buf, years, "year", "years");
    if (years < 1000) return span(buf, years / 100, "century", "centuries");
    if (years < 1e6) return span(buf, years / 1000, "millennium", "millennia");

    // years >= 1e6 here, so centuries >= 10,000: collapse to a scaled plural.
    const centuries = years / 100;
    for (scales) |s| {
        if (centuries >= s.factor) {
            const scaled = round(centuries / s.factor);
            const fmt: []const u8 = if (scaled == @trunc(scaled) or scaled >= 10.0) "{d:.0} {s} centuries" else "{d:.1} {s} centuries";
            return std.fmt.bufPrint(buf, fmt, .{ scaled, s.name }) catch unreachable;
        }
    }
    return span(buf, centuries, "century", "centuries");
}

/// Generate a diceware passphrase from the EFF long wordlist.
/// `wordlist` must hold exactly 7,776 entries. Caller owns every slice in
/// the returned Passphrase (free `passphrase`, `crack_time_human`, `words`,
/// and each capitalized word copy with `allocator`).
pub fn generatePassphrase(
    allocator: std.mem.Allocator,
    wordlist: []const []const u8,
    options: PassphraseOptions,
) std.mem.Allocator.Error!Passphrase {
    const word_count = @min(max_words, @max(min_words, options.word_count));
    const sep = options.separator.str();

    const words = try allocator.alloc([]const u8, word_count);
    errdefer allocator.free(words);

    var total_len: usize = 0;
    for (0..word_count) |i| {
        const w = randomWord(wordlist);
        const shown = if (options.capitalize) try capitalizeWord(allocator, w) else w;
        words[i] = shown;
        total_len += shown.len;
    }
    if (word_count > 1) total_len += sep.len * (word_count - 1);
    if (options.append_digit) total_len += 1;

    const passphrase = try allocator.alloc(u8, total_len);
    errdefer allocator.free(passphrase);
    {
        var p: usize = 0;
        for (words, 0..) |w, i| {
            if (i > 0) {
                @memcpy(passphrase[p .. p + sep.len], sep);
                p += sep.len;
            }
            @memcpy(passphrase[p .. p + w.len], w);
            p += w.len;
        }
        if (options.append_digit) {
            passphrase[p] = randomDigit();
            p += 1;
        }
    }

    const entropy = entropyBits(word_count);
    const seconds = crackTimeSeconds(entropy, options.guesses_per_second);
    var human_buf: [64]u8 = undefined;
    const human = try allocator.dupe(u8, formatCrackTime(&human_buf, seconds));

    return .{
        .words = words,
        .passphrase = passphrase,
        .entropy = entropy,
        .crack_time_seconds = seconds,
        .crack_time_human = human,
    };
}

/// Uppercase the first byte (EFF words are ASCII, so one byte is one char).
fn capitalizeWord(allocator: std.mem.Allocator, w: []const u8) std.mem.Allocator.Error![]u8 {
    const out = try allocator.dupe(u8, w);
    if (out.len > 0) out[0] = std.ascii.toUpper(out[0]);
    return out;
}

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 →