Skip to content

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

"""password-generator — Python 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 supported
language.

This file is display source — part of CosmoDev's polyglot tool pages
(dev.cosmolabs.org). License: MIT.
"""

from __future__ import annotations

import math
import secrets
from dataclasses import dataclass

# Character pools. Plain strings: building a charset is concatenation, and
# iterating yields the individual glyphs directly.
LOWER = "abcdefghijklmnopqrstuvwxyz"
UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
NUMBERS = "0123456789"
SYMBOLS = "!@#$%^&*()-_=+[]{};:,.<>?/"

# Visually ambiguous glyphs (O/0, I/l/1, and a stray pipe) dropped when the
# caller asks to harden the pool. Exactly the set behind the TS regex
# /[O0Il1|]/g — note lowercase 'o' and uppercase 'L' are intentionally NOT
# included. frozenset gives O(1) membership with no mutation surface.
_AMBIGUOUS = frozenset("O0Il1|")

# 2^32 — the Uint32 draw range used by rejection sampling (exclusive).
_UINT32_RANGE = 1 << 32


@dataclass
class PasswordOptions:
    """Generator configuration — field-for-field with the TS interface.

    Boolean classes default off so an explicit, mindful choice is required
    before any password can be produced (an empty class set yields "").
    """

    length: int
    upper: bool = False
    lower: bool = False
    numbers: bool = False
    symbols: bool = False
    exclude_ambiguous: bool = False


@dataclass
class PasswordStrength:
    """Tier assessment handed back to the UI for colour-coding."""

    label: str
    variant: str  # "danger" | "accent" | "success"
    segments: int  # 1..5


@dataclass
class AttackScenario:
    """One attack model's guess rate."""

    id: str
    label: str
    guesses_per_second: float


def build_charset(o: PasswordOptions) -> str:
    """Assemble the candidate alphabet from the selected option flags.

    The lower -> upper -> digit -> symbol order is cosmetic: every draw picks
    a uniform index over the surviving pool, so ordering affects only *which*
    characters are available, never their relative frequency.
    """
    cs = ""
    if o.lower:
        cs += LOWER
    if o.upper:
        cs += UPPER
    if o.numbers:
        cs += NUMBERS
    if o.symbols:
        cs += SYMBOLS
    if o.exclude_ambiguous:
        cs = "".join(c for c in cs if c not in _AMBIGUOUS)
    return cs


def _unbiased_index(n: int) -> int:
    """Return a uniform index in [0, n) via rejection sampling.

    Draw a 32-bit value from :mod:`secrets` (Python's CSPRNG, backed by the OS
    entropy source — ``getrandom`` on Linux, ``SecRandomCopyBytes`` on macOS,
    ``CryptGenRandom`` on Windows) 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 :func:`random.choice` — its Mersenne-Twister is
    deterministic and predictable. Mirrors ``unbiasedIndex`` in the TS lib.
    """
    limit = _UINT32_RANGE - (_UINT32_RANGE % n)
    while True:
        r = secrets.randbits(32)  # uniform in [0, 2^32)
        if r < limit:
            return r % n


def generate_password(o: PasswordOptions) -> str:
    """Return a cryptographically-random, unbiased password of ``o.length`` chars.

    Each character index is drawn with rejection sampling over :mod:`secrets`,
    so every position is uniformly distributed over the charset. Returns ""
    when no character class is enabled or length < 1.
    """
    cs = build_charset(o)
    if not cs or o.length < 1:
        return ""
    return "".join(cs[_unbiased_index(len(cs))] for _ in range(o.length))


# Entropy tier thresholds (bits), 1:1 with the 5 strength-meter segments.
_TIER_VERY_STRONG = 100
_TIER_STRONG = 70
_TIER_FAIR = 45
_TIER_WEAK = 28


def entropy_bits(length: int, charset_size: int) -> float:
    """Theoretical entropy (bits) of a uniform-random password.

    Shannon formula: ``length * log2(|alphabet|)``. Returns 0.0 for a
    non-positive length or a charset size <= 1.
    """
    if length <= 0 or charset_size <= 1:
        return 0.0
    return length * math.log2(charset_size)


def strength_tier(bits: float) -> PasswordStrength:
    """Classify an entropy value into one of five tiers, 1:1 with the meter."""
    if bits >= _TIER_VERY_STRONG:
        return PasswordStrength("very strong", "success", 5)
    if bits >= _TIER_STRONG:
        return PasswordStrength("strong", "success", 4)
    if bits >= _TIER_FAIR:
        return PasswordStrength("fair", "accent", 3)
    if bits >= _TIER_WEAK:
        return PasswordStrength("weak", "danger", 2)
    return PasswordStrength("very weak", "danger", 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.
ATTACK_SCENARIOS = [
    AttackScenario("online-throttled", "online, throttled (100/h)", 100 / 3600),
    AttackScenario("online", "online, no throttle (10/s)", 10),
    AttackScenario("offline-slow", "offline, slow hash (10⁴/s)", 1e4),
    AttackScenario("offline-fast", "offline, fast GPU (10¹⁰/s)", 1e10),
]


def crack_time_seconds(bits: float, guesses_per_second: float) -> float:
    """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``).
    """
    return 2 ** (bits - 1) / guesses_per_second

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 →