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 generation + entropy scoring.
// C# 12 / .NET 8 — ported from src/lib/passphrase-generator.ts (the canonical
// TypeScript implementation). Display source for CosmoDev's polyglot pages.
//
// Word selection uses the CSPRNG (RandomNumberGenerator) with rejection
// sampling, so every word index is uniform over the 7,776-word EFF long
// wordlist — no modulo bias.

/// <summary>The five separator styles offered in the UI.</summary>
public enum Separator
{
    Space,
    Dash,
    Dot,
    Underscore,
    None,
}

/// <summary>Passphrase generation options; every field has a default.</summary>
public sealed record PassphraseOptions(
    int? WordCount = null,
    Separator Separator = Separator.Space,
    bool Capitalize = false,
    bool AppendDigit = false,
    double GuessesPerSecond = PassphraseGenerator.DefaultGuessesPerSecond);

/// <summary>Human-readable crack-time span.</summary>
/// <param name="Seconds">Average time to crack, in seconds.</param>
/// <param name="Human">The formatted span, e.g. "2.4 billion centuries".</param>
public sealed record CrackTimeEstimate(double Seconds, string Human);

/// <summary>A generated passphrase with its entropy analysis.</summary>
/// <param name="Words">The drawn words, in order (uncapitalized).</param>
/// <param name="Passphrase">The final passphrase string.</param>
/// <param name="Entropy">Theoretical word entropy in bits.</param>
/// <param name="CrackTime">Average-time-to-crack estimate.</param>
public sealed record Passphrase(string[] Words, string Passphrase, double Entropy, CrackTimeEstimate CrackTime);

public static class PassphraseGenerator
{
    /// <summary>Size of the EFF long wordlist (6^5).</summary>
    public const int EffWordlistLength = 7776;

    public const int MinWords = 3;
    public const int MaxWords = 10;
    public const int DefaultWords = 6;
    public const double DefaultGuessesPerSecond = 1e12;

    private static string SeparatorChar(Separator s) => s switch
    {
        Separator.Space => " ",
        Separator.Dash => "-",
        Separator.Dot => ".",
        Separator.Underscore => "_",
        Separator.None => "",
        _ => throw new ArgumentOutOfRangeException(nameof(s)),
    };

    // The EFF long wordlist ships as an embedded resource (7,776 lines).
    // https://www.eff.org/deeplinks/2016/07/new-wordlists-random-passphrases
    private static readonly string[] Words = LoadWordlist();

    private static string[] LoadWordlist()
    {
        var assembly = typeof(PassphraseGenerator).Assembly;
        using var stream = assembly.GetManifestResourceStream("CosmoDev.eff-long.txt")!;
        using var reader = new StreamReader(stream);
        var list = new List<string>(EffWordlistLength);
        while (reader.ReadLine() is { } line) list.Add(line);
        return list.ToArray();
    }

    // Rejection-sampling ceiling for a uniform uint16 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).
    private const int WordLimit = EffWordlistLength * (65536 / EffWordlistLength);

    /// <summary>Draw one uniformly-random word from the EFF long wordlist (CSPRNG).</summary>
    private static string RandomWord()
    {
        Span<byte> buf = stackalloc byte[2];
        int v;
        do
        {
            System.Security.Cryptography.RandomNumberGenerator.Fill(buf);
            v = BitConverter.ToUInt16(buf);
        } while (v >= WordLimit);
        return Words[v % EffWordlistLength];
    }

    /// <summary>Draw one uniformly-random digit 0-9 (CSPRNG, rejection-sampled on uint8).</summary>
    private static string RandomDigit()
    {
        Span<byte> buf = stackalloc byte[1];
        int v;
        do
        {
            System.Security.Cryptography.RandomNumberGenerator.Fill(buf);
            v = buf[0];
        } while (v >= 250); // largest multiple of 10 in [0, 256)
        return (v % 10).ToString();
    }

    /// <summary>Theoretical word entropy in bits: wordCount × log2(7776).</summary>
    public static double EntropyBits(int wordCount) =>
        wordCount <= 0 ? 0 : wordCount * Math.Log2(EffWordlistLength);

    private const double Year = 31_557_600; // seconds (Julian year)

    private static readonly (double Factor, string Name)[] Scales =
    [
        (1e12, "trillion"),
        (1e9, "billion"),
        (1e6, "million"),
        (1e3, "thousand"),
    ];

    /// <summary>
    /// 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).
    /// </summary>
    public static string FormatCrackTime(double seconds)
    {
        if (!double.IsFinite(seconds) || seconds < 0) return "-";
        if (seconds < 1) return "< 1 second";
        if (seconds < 60) return Span(seconds, "second");
        if (seconds < 3600) return Span(seconds / 60, "minute");
        if (seconds < 86_400) return Span(seconds / 3600, "hour");
        if (seconds < Year) return Span(seconds / 86_400, "day");
        var years = seconds / Year;
        if (years < 100) return Span(years, "year");
        if (years < 1000) return Span(years / 100, "century", "centuries");
        if (years < 1e6) return Span(years / 1000, "millennium", "millennia");
        // years >= 1e6 here, so centuries >= 10,000: collapse to a scaled plural.
        var centuries = years / 100;
        var (factor, name) = Scales.First(s => centuries >= s.Factor);
        return $"{Format(centuries / factor)} {name} centuries";
    }

    /// <summary>Round + pluralize "v unit"; 1 decimal below 10, whole numbers above.</summary>
    private static string Span(double v, string singular, string? plural = null)
    {
        plural ??= $"{singular}s";
        var n = Round(v);
        return $"{Format(n)} {(n == 1 ? singular : plural)}";
    }

    private static double Round(double v) => v >= 10 ? Math.Round(v) : Math.Round(v, 1);

    /// <summary>Group separators like JS toLocaleString(): 1 decimal below 10, none above.</summary>
    private static string Format(double n) => n >= 10 ? n.ToString("N0") : n.ToString("N1");

    /// <summary>Average time to crack (seconds) = 2^(bits-1) / guessesPerSecond.</summary>
    public static double CrackTimeSeconds(double bits, double guessesPerSecond) =>
        Math.Pow(2, bits - 1) / guessesPerSecond;

    /// <summary>Generate a diceware passphrase from the EFF long wordlist.</summary>
    public static Passphrase Generate(PassphraseOptions? options = null)
    {
        options ??= new PassphraseOptions();
        var raw = options.WordCount ?? DefaultWords;
        var wordCount = Math.Clamp(
            double.IsFinite(raw) ? (int)Math.Round((double)raw) : DefaultWords,
            MinWords, MaxWords);
        var separator = SeparatorChar(options.Separator);

        var words = Enumerable.Range(0, wordCount).Select(_ => RandomWord()).ToArray();
        var shown = options.Capitalize ? words.Select(Cap).ToArray() : words;
        var passphrase = string.Join(separator, shown);
        if (options.AppendDigit) passphrase += RandomDigit();

        var entropy = EntropyBits(wordCount);
        var seconds = CrackTimeSeconds(entropy, options.GuessesPerSecond);
        return new Passphrase(words, passphrase, entropy,
            new CrackTimeEstimate(seconds, FormatCrackTime(seconds)));
    }

    private static string Cap(string w) => char.ToUpperInvariant(w[0]) + w[1..];
}

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 →