Skip to content

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

<?php
/**
 * Passphrase Generator — diceware generation + entropy scoring.
 *
 * Language: PHP (8.1+, standard library only)
 * 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.
 *
 * Word selection uses random_bytes() (CSPRNG) with rejection sampling, so
 * every word index is uniform over the 7,776-word EFF long wordlist — no
 * modulo bias. The wordlist is too large to inline; pass it in as an array
 * of strings (one word per line of the EFF list).
 */

declare(strict_types=1);

const EFF_WORDLIST_LENGTH = 7776; // 6^5
const MIN_WORDS = 3;
const MAX_WORDS = 10;
const DEFAULT_WORDS = 6;
const DEFAULT_GUESSES_PER_SECOND = 1e12;

const SEPARATORS = [
    'space' => ' ',
    'dash' => '-',
    'dot' => '.',
    'underscore' => '_',
    'none' => '',
];

// Largest multiple of 7,776 that fits in [0, 65536): 7776 * 8 = 62208.
// Draws at or above the limit are rejected so v % 7776 is uniform.
const WORD_LIMIT = EFF_WORDLIST_LENGTH * 8;

const YEAR = 31557600; // seconds (Julian year)
const SCALES = [
    [1e12, 'trillion'],
    [1e9, 'billion'],
    [1e6, 'million'],
    [1e3, 'thousand'],
];

/** One uniformly-random word from the EFF long wordlist (CSPRNG). */
function random_word(array $wordlist): string
{
    do {
        $v = unpack('n', random_bytes(2))[1]; // big-endian uint16
    } while ($v >= WORD_LIMIT);
    return $wordlist[$v % EFF_WORDLIST_LENGTH];
}

/** One uniformly-random digit 0-9 (CSPRNG, rejection-sampled on uint8). */
function random_digit(): string
{
    do {
        $v = ord(random_bytes(1));
    } while ($v >= 250); // largest multiple of 10 in [0, 256)
    return (string) ($v % 10);
}

/** Theoretical word entropy in bits: wordCount * log2(7776). */
function entropy_bits(int|float $word_count): float
{
    return $word_count <= 0 ? 0.0 : $word_count * log(EFF_WORDLIST_LENGTH, 2);
}

/** Average time to crack (seconds) = 2^(bits-1) / guessesPerSecond. */
function crack_time_seconds(float $bits, ?float $guesses_per_second = null): float
{
    $gps = $guesses_per_second ?? DEFAULT_GUESSES_PER_SECOND;
    return 2 ** ($bits - 1) / $gps;
}

function span(float $v, string $singular, ?string $plural = null): string
{
    $n = round_value($v);
    $unit = $n === 1 ? $singular : ($plural ?? $singular . 's');
    return locale($n) . ' ' . $unit;
}

function round_value(float $v): int|float
{
    // 1 decimal below 10, whole numbers above.
    return $v >= 10 ? round($v) : round($v, 1);
}

function locale(int|float $n): string
{
    if (is_float($n) && $n != (int) $n && (int) $n < 1000) {
        return number_format($n, 1); // e.g. "9.5"
    }
    return number_format((int) $n); // grouped integer, e.g. "12,345"
}

/** Human-readable crack-time span; collapses to scaled words at diceware
 *  entropies so the string stays readable ("2.4 billion centuries"). */
function format_crack_time(float $seconds): string
{
    if ($seconds < 0 || is_infinite($seconds) || is_nan($seconds)) return '-';
    if ($seconds < 1) return '< 1 second';
    if ($seconds < 60) return span($seconds, 'second');
    if ($seconds < 3600) return span($seconds / 60, 'minute');
    if ($seconds < 86400) return span($seconds / 3600, 'hour');
    if ($seconds < YEAR) return span($seconds / 86400, 'day');

    $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');

    $centuries = $years / 100; // >= 10,000 here: collapse to a scaled plural
    foreach (SCALES as [$factor, $name]) {
        if ($centuries >= $factor) {
            return locale(round_value($centuries / $factor)) . " $name centuries";
        }
    }
    return locale(round_value($centuries)) . ' centuries';
}

/**
 * Generate a diceware passphrase from the EFF long wordlist. $options keys
 * (all optional): word_count, separator ('space'|'dash'|'dot'|'underscore'|
 * 'none'), capitalize, append_digit, guesses_per_second (1e12 default).
 * Returns [words, passphrase, entropy, crack_time => [seconds, human]].
 */
function generate_passphrase(array $wordlist, array $options = []): array
{
    $raw = $options['word_count'] ?? DEFAULT_WORDS;
    $count = is_numeric($raw) ? (int) round((float) $raw) : DEFAULT_WORDS;
    $count = min(MAX_WORDS, max(MIN_WORDS, $count));
    $sep = SEPARATORS[$options['separator'] ?? 'space'] ?? ' ';
    $capitalize = (bool) ($options['capitalize'] ?? false);
    $appendDigit = (bool) ($options['append_digit'] ?? false);
    $gps = (float) ($options['guesses_per_second'] ?? DEFAULT_GUESSES_PER_SECOND);

    $words = [];
    for ($i = 0; $i < $count; $i++) {
        $words[] = random_word($wordlist);
    }
    $shown = $capitalize
        ? array_map(fn (string $w): string => ucfirst($w), $words)
        : $words;
    $passphrase = implode($sep, $shown);
    if ($appendDigit) {
        $passphrase .= random_digit();
    }

    $entropy = entropy_bits($count);
    $seconds = crack_time_seconds($entropy, $gps);
    return [
        'words' => $words,
        'passphrase' => $passphrase,
        'entropy' => $entropy,
        'crack_time' => ['seconds' => $seconds, 'human' => format_crack_time($seconds)],
    ];
}

// Example (deterministic parts only):
//   entropy_bits(6);                                    // 77.548875... bits
//   format_crack_time(crack_time_seconds(entropy_bits(6)));
//   // -> "3.5 millennia" (at 1e12 guesses/s)
//   format_crack_time(crack_time_seconds(entropy_bits(8)));
//   // -> "2.1 billion centuries"
//   generate_passphrase($effWordlist, ['separator' => 'dash',
//                                      'capitalize' => true,
//                                      'append_digit' => true]);
//   // -> passphrase "Oatmeal-Marker-Ranger-...-7", entropy 77.55

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 →