Skip to content

Password Generator — PHP source

Generate cryptographically-random passwords with a CSPRNG using rejection sampling (no modulo bias). Shows live entropy in bits, a 5-tier strength meter, average offline-GPU crack time, and a Pro mode with the entropy formula, a crack-time-vs-length curve, and a 4-scenario attack table. 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
/**
 * password-generator — PHP polyglot showcase port.
 *
 * Cryptographically-secure password generation with entropy scoring and an
 * average crack-time model. Mirrors the canonical TypeScript implementation at
 *   src/lib/password.ts
 * so the CosmoDev tool pages show equivalent logic across every language.
 *
 * This file is display source — part of CosmoDev's polyglot tool pages
 * (dev.cosmolabs.org). License: MIT.
 */

declare(strict_types=1);

namespace CosmoDev\Password;

/**
 * PasswordGenerator
 *
 * Static facade over the pure generation + scoring logic. Static methods keep
 * the public surface flat — call sites read as `PasswordGenerator::generate(...)`
 * — without forcing callers to manage instance state, of which there is none.
 */
final class PasswordGenerator
{
    /** Lowercase pool. */
    private const LOWER = 'abcdefghijklmnopqrstuvwxyz';
    /** Uppercase pool. */
    private const UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
    /** Digit pool. */
    private const NUMBERS = '0123456789';
    /** Symbol pool (matches the TS SETS.symbols string exactly). */
    private const SYMBOLS = '!@#$%^&*()-_=+[]{};:,.<>?/';

    /**
     * Glyphs that are easy to confuse by eye, dropped when `excludeAmbiguous`
     * is set. Mirrors the TS regex /[O0Il1|]/g — the set is {O, 0, I, l, 1, |};
     * lowercase 'o' and uppercase 'L' are intentionally NOT removed.
     */
    private const AMBIGUOUS = ['O', '0', 'I', 'l', '1', '|'];

    /** 2^32 — the Uint32 draw range used by rejection sampling (exclusive upper bound). */
    private const UINT32_RANGE = 1 << 32; // 4294967296 on 64-bit PHP.

    /** Entropy tier thresholds, in bits. */
    private const TIER_VERY_STRONG = 100;
    private const TIER_STRONG = 70;
    private const TIER_FAIR = 45;
    private const TIER_WEAK = 28;

    /**
     * Option shape: array{length:int, upper:bool, lower:bool,
     * numbers:bool, symbols:bool, excludeAmbiguous:bool}.
     */

    /**
     * Build the candidate alphabet from the option flags.
     *
     * The lower -> upper -> digit -> symbol ordering is cosmetic; every draw
     * is a uniform index over the surviving pool, so order never affects the
     * distribution — only membership does.
     *
     * @param array $options {@see PasswordGenerator} option shape.
     * @return string
     */
    public static function buildCharset(array $options): string
    {
        $cs = '';
        if ($options['lower'] ?? false) {
            $cs .= self::LOWER;
        }
        if ($options['upper'] ?? false) {
            $cs .= self::UPPER;
        }
        if ($options['numbers'] ?? false) {
            $cs .= self::NUMBERS;
        }
        if ($options['symbols'] ?? false) {
            $cs .= self::SYMBOLS;
        }

        if ($options['excludeAmbiguous'] ?? false) {
            // str_replace with an array of needles runs in a single pass —
            // cheaper than a regex for a fixed, tiny blacklist.
            $cs = str_replace(self::AMBIGUOUS, '', $cs);
        }

        return $cs;
    }

    /**
     * Return a uniform index in [0, n) via rejection sampling.
     *
     * Draw a 32-bit value from `random_int()` (PHP's CSPRNG, backed by the OS
     * entropy source since PHP 7.0) and reject any value at or above `limit`
     * — the largest multiple of n that fits in the 2^32 range — so the
     * survivors reduce evenly onto [0, n). This eliminates the modulo bias of
     * a plain `draw % n` (which over-weights the low buckets when 2^32 is not
     * a multiple of n). Never use `rand()` / `mt_rand()` (Mersenne-Twister,
     * predictable) for secrets.
     *
     * @param int $n Pool size (must be > 0).
     * @return int  Uniform index in [0, n).
     */
    private static function unbiasedIndex(int $n): int
    {
        $limit = self::UINT32_RANGE - (self::UINT32_RANGE % $n);
        do {
            // random_int's bounds are inclusive; UINT32_RANGE - 1 = 2^32 - 1.
            $r = random_int(0, self::UINT32_RANGE - 1); // uniform in [0, 2^32)
        } while ($r >= $limit);
        return $r % $n;
    }

    /**
     * Generate a cryptographically-random, unbiased password.
     *
     * Each character index is drawn with rejection sampling over
     * `random_int()`, so every position is uniformly distributed over the
     * charset. Returns '' when no class is enabled or length < 1.
     *
     * @param array $options {@see PasswordGenerator} option shape.
     * @return string
     */
    public static function generate(array $options): string
    {
        $cs = self::buildCharset($options);
        $length = $options['length'] ?? 0;
        if ($cs === '' || $length < 1) {
            return '';
        }

        $n = strlen($cs);
        $out = '';
        for ($i = 0; $i < $length; $i++) {
            $out .= $cs[self::unbiasedIndex($n)];
        }

        return $out;
    }

    /**
     * Theoretical entropy (bits) of a uniform-random password — the Shannon
     * formula: `length * log2(|alphabet|)`. Returns 0.0 for a non-positive
     * length or a charset size <= 1.
     *
     * @param int $length
     * @param int $charsetSize
     * @return float
     */
    public static function entropyBits(int $length, int $charsetSize): float
    {
        if ($length <= 0 || $charsetSize <= 1) {
            return 0.0;
        }
        // log(x, 2) is log base 2.
        return $length * log($charsetSize, 2);
    }

    /**
     * Classify an entropy value into one of five tiers, 1:1 with the meter.
     *
     * @param float $bits
     * @return array{label:string,variant:string,segments:int}
     */
    public static function strengthTier(float $bits): array
    {
        if ($bits >= self::TIER_VERY_STRONG) {
            return ['label' => 'very strong', 'variant' => 'success', 'segments' => 5];
        }
        if ($bits >= self::TIER_STRONG) {
            return ['label' => 'strong', 'variant' => 'success', 'segments' => 4];
        }
        if ($bits >= self::TIER_FAIR) {
            return ['label' => 'fair', 'variant' => 'accent', 'segments' => 3];
        }
        if ($bits >= self::TIER_WEAK) {
            return ['label' => 'weak', 'variant' => 'danger', 'segments' => 2];
        }
        return ['label' => 'very weak', 'variant' => 'danger', 'segments' => 1];
    }

    /**
     * The four documented attack models, from a throttled online attacker to a
     * fast offline GPU rig. Guess rates match the TS ATTACK_SCENARIOS constant.
     *
     * @return array<int,array{id:string,label:string,guessesPerSecond:float}>
     */
    public static function attackScenarios(): array
    {
        return [
            ['id' => 'online-throttled', 'label' => 'online, throttled (100/h)', 'guessesPerSecond' => 100 / 3600],
            ['id' => 'online', 'label' => 'online, no throttle (10/s)', 'guessesPerSecond' => 10],
            ['id' => 'offline-slow', 'label' => 'offline, slow hash (10⁴/s)', 'guessesPerSecond' => 1e4],
            ['id' => 'offline-fast', 'label' => 'offline, fast GPU (10¹⁰/s)', 'guessesPerSecond' => 1e10],
        ];
    }

    /**
     * Average time to crack (seconds). `2^(bits-1)` averages over the keyspace
     * — on average half the space is searched before the secret is found — so
     * this is the EXPECTED time, not the worst-case full-keyspace search
     * (`2^bits / rate`).
     *
     * @param float $bits
     * @param float $guessesPerSecond
     * @return float
     */
    public static function crackTimeSeconds(float $bits, float $guessesPerSecond): float
    {
        return pow(2, $bits - 1) / $guessesPerSecond;
    }
}

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 →