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 →