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 →