Passphrase Generator — Python 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 Python implementation — the same logic the interactive tool runs, in a shareable, citable form.
"""Passphrase Generator — diceware generation + entropy scoring.
Language: Python (3.9+, 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 the ``secrets`` 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 a list of strings (one
word per line of the EFF list).
"""
import math
import secrets
# Size of the EFF long wordlist (6^5).
EFF_WORDLIST_LENGTH = 7776
MIN_WORDS = 3
MAX_WORDS = 10
DEFAULT_WORDS = 6
DEFAULT_GUESSES_PER_SECOND = 1e12
SEPARATORS = {
"space": " ",
"dash": "-",
"dot": ".",
"underscore": "_",
"none": "",
}
# Rejection-sampling ceiling for a uniform uint16 over 7,776: the largest
# multiple of 7,776 that fits in [0, 65536). 7776 x 8 = 62208; draws >= 62208
# are rejected so `v % 7776` is uniform.
WORD_LIMIT = EFF_WORDLIST_LENGTH * (65536 // EFF_WORDLIST_LENGTH)
YEAR = 31_557_600 # seconds (Julian year)
SCALES = [
(1e12, "trillion"),
(1e9, "billion"),
(1e6, "million"),
(1e3, "thousand"),
]
def random_word(wordlist):
"""One uniformly-random word from the EFF long wordlist (CSPRNG)."""
while True:
v = secrets.randbits(16)
if v < WORD_LIMIT:
return wordlist[v % EFF_WORDLIST_LENGTH]
def random_digit():
"""One uniformly-random digit 0-9 (CSPRNG, rejection-sampled on uint8)."""
while True:
v = secrets.randbits(8)
if v < 250: # largest multiple of 10 in [0, 256)
return str(v % 10)
def entropy_bits(word_count):
"""Theoretical word entropy in bits: wordCount * log2(7776)."""
return 0 if word_count <= 0 else word_count * math.log2(EFF_WORDLIST_LENGTH)
def crack_time_seconds(bits, guesses_per_second=DEFAULT_GUESSES_PER_SECOND):
"""Average time to crack (seconds) = 2^(bits-1) / guessesPerSecond."""
return 2 ** (bits - 1) / guesses_per_second
def _round(v):
"""1 decimal below 10, whole numbers above."""
return round(v) if v >= 10 else round(v, 1)
def _locale(n):
"""Thousands-grouped rendering; floats below 1000 keep their decimal."""
i = int(n)
return f"{n:,}" if n != i and i < 1000 else f"{i:,}"
def _span(v, singular, plural=None):
n = _round(v)
unit = singular if n == 1 else (plural or f"{singular}s")
return f"{_locale(n)} {unit}"
def format_crack_time(seconds):
"""Human-readable crack-time span; collapses to scaled words at diceware
entropies so the string stays readable ("2.4 billion centuries")."""
if not math.isfinite(seconds) or seconds < 0:
return "-"
if seconds < 1:
return "< 1 second"
if seconds < 60:
return _span(seconds, "second")
if seconds < 3600:
return _span(seconds / 60, "minute")
if seconds < 86_400:
return _span(seconds / 3600, "hour")
if seconds < YEAR:
return _span(seconds / 86_400, "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
factor, name = next((f, n) for f, n in SCALES if centuries >= f)
return f"{_locale(_round(centuries / factor))} {name} centuries"
def generate_passphrase(wordlist, *, word_count=None, separator="space",
capitalize=False, append_digit=False,
guesses_per_second=DEFAULT_GUESSES_PER_SECOND):
"""Generate a diceware passphrase from the EFF long wordlist.
Returns a dict: words, passphrase, entropy, and a crack_time estimate
{seconds, human}. Mirrors generatePassphrase() in the TS reference.
"""
raw = DEFAULT_WORDS if word_count is None else word_count
count = round(raw) if isinstance(raw, (int, float)) else DEFAULT_WORDS
count = min(MAX_WORDS, max(MIN_WORDS, count))
sep = SEPARATORS.get(separator, " ")
gps = guesses_per_second or DEFAULT_GUESSES_PER_SECOND
words = [random_word(wordlist) for _ in range(count)]
shown = [w[0].upper() + w[1:] for w in words] if capitalize else words
passphrase = sep.join(shown)
if append_digit:
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)},
}
if __name__ == "__main__":
# Deterministic parts of the contract:
print(entropy_bits(6)) # 77.548875... bits
print(format_crack_time(crack_time_seconds(entropy_bits(6))))
# -> "3.5 millennia" (at 1e12 guesses/s)
print(format_crack_time(crack_time_seconds(entropy_bits(8))))
# -> "2.1 billion centuries"
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 →