Skip to content

Latest commit

 

History

History
295 lines (222 loc) · 11.7 KB

File metadata and controls

295 lines (222 loc) · 11.7 KB

Password Generator

A cryptographically secure, policy-driven password and passphrase generator built on the secrets module with an argparse CLI.

Project Overview

Strong credentials must be generated with a cryptographically secure random source, not random. This project uses Python's secrets module to build passwords that satisfy a configurable policy — length, character classes, and guaranteed inclusion of each required class — plus a diceware-style passphrase mode. It teaches the difference between random and secrets, and how to reason about password entropy.

[!warning] Handle generated secrets carefully Treat generated passwords as sensitive: don't log them, echo them into shared terminals, or store them in plaintext. This tool is for your own account hardening and lab use.

Objectives

  • Generate passwords using a cryptographically secure random source.
  • Guarantee required character classes without weakening randomness.
  • Report entropy in bits and translate it into a verdict.
  • Support passphrase generation as well as random strings.
  • Avoid exposing generated secrets through logs, history, or arguments.

Requirements

Item Detail
Python 3.9 or newer
Dependencies secrets, string, math, argparse (standard library)
Privileges None
Wordlist Optional, for passphrase mode — a large public list such as EFF's
Note Uses secrets, never random

Architecture

  • CLIargparse for length, count, character-class toggles, and passphrase mode.
  • Policy builder — assembles the allowed alphabet from selected classes and records "required" classes.
  • Generator — draws characters with secrets.choice(); re-rolls until every required class appears.
  • Passphrase modesecrets.choice() over a word list joined by a separator.
  • Entropy estimator — reports approximate bits of entropy for the chosen policy.
policy (classes, length) ─▶ secrets.choice loop ─▶ satisfies policy? ─▶ output + entropy

Directory Structure

password-generator/
├── passgen.py          # CLI + generator
└── wordlist.txt        # (optional) words for passphrase mode

Implementation Plan

  1. Generate. Build a password with secrets.choice() over a defined alphabet.
  2. Enforce classes. Guarantee at least one character from each required class, then shuffle with secrets.SystemRandom().shuffle() so position is not predictable.
  3. Compute entropy. length * log2(alphabet_size) bits, reported alongside the password.
  4. Add a verdict. Map entropy bands to labels and explain what would improve the score.
  5. Add passphrase mode. Select N words uniformly from a large wordlist; entropy is N * log2(wordlist_size).
  6. Support bulk generation with a --count flag.
  7. Protect the output. Never log generated values; support writing to a file with restrictive permissions.

Complete Example Implementation

#!/usr/bin/env python3
"""passgen.py - Cryptographically secure password/passphrase generator.

Uses secrets (CSPRNG), never random, for credential generation.
"""
import argparse
import math
import secrets
import string

CLASSES = {
    "lower": string.ascii_lowercase,
    "upper": string.ascii_uppercase,
    "digits": string.digits,
    "symbols": "!@#$%^&*()-_=+[]{};:,.?",
}


def build_alphabet(use: dict[str, bool]) -> tuple[str, list[str]]:
    """Return (alphabet, required_classes) from enabled character classes."""
    alphabet = ""
    required = []
    for name, chars in CLASSES.items():
        if use[name]:
            alphabet += chars
            required.append(chars)
    if not alphabet:
        raise ValueError("at least one character class must be enabled")
    return alphabet, required


def generate_password(length: int, use: dict[str, bool]) -> str:
    """Generate a password guaranteeing at least one char per enabled class."""
    alphabet, required = build_alphabet(use)
    if length < len(required):
        raise ValueError(f"length must be >= {len(required)} for this policy")
    while True:
        pw = "".join(secrets.choice(alphabet) for _ in range(length))
        if all(any(c in cls for c in pw) for cls in required):
            return pw


def generate_passphrase(words: list[str], count: int, sep: str) -> str:
    """Diceware-style passphrase from a word list."""
    return sep.join(secrets.choice(words) for _ in range(count))


def entropy_bits(alphabet_size: int, length: int) -> float:
    """Approximate password entropy in bits: length * log2(alphabet)."""
    return length * math.log2(alphabet_size)


def main() -> int:
    parser = argparse.ArgumentParser(description="Secure password generator.")
    parser.add_argument("-l", "--length", type=int, default=16, help="password length")
    parser.add_argument("-c", "--count", type=int, default=1, help="how many to generate")
    parser.add_argument("--no-lower", action="store_true", help="disable lowercase")
    parser.add_argument("--no-upper", action="store_true", help="disable uppercase")
    parser.add_argument("--no-digits", action="store_true", help="disable digits")
    parser.add_argument("--no-symbols", action="store_true", help="disable symbols")
    parser.add_argument("--passphrase", metavar="WORDLIST",
                        help="passphrase mode using the given word list file")
    parser.add_argument("--words", type=int, default=5, help="words per passphrase")
    args = parser.parse_args()

    if args.passphrase:
        with open(args.passphrase, encoding="utf-8") as fh:
            words = [w.strip() for w in fh if w.strip()]
        for _ in range(args.count):
            print(generate_passphrase(words, args.words, "-"))
        return 0

    use = {
        "lower": not args.no_lower,
        "upper": not args.no_upper,
        "digits": not args.no_digits,
        "symbols": not args.no_symbols,
    }
    alphabet, _ = build_alphabet(use)
    for _ in range(args.count):
        print(generate_password(args.length, use))
    print(f"[*] ~{entropy_bits(len(alphabet), args.length):.0f} bits of entropy "
          f"(alphabet={len(alphabet)}, length={args.length})")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
python passgen.py -l 20 -c 3
python passgen.py --passphrase /usr/share/dict/words --words 6

Output

K7!pv#Qm2@rLd9Xs&Tn0
b4$WcH8^uZ1yE-Rg3Jq!
xP2&mN9dV6@kL!sQ7wYt
[*] ~131 bits of entropy (alphabet=88, length=20)

Usage

# Single 20-character password
python3 pwgen.py --length 20

# Require every character class
python3 pwgen.py --length 24 --require-all

# Alphanumeric only, for systems that reject symbols
python3 pwgen.py --length 32 --no-symbols

# Six-word passphrase
python3 pwgen.py --passphrase --words 6 --wordlist eff_large_wordlist.txt

# Ten passwords, written with 0600 permissions
python3 pwgen.py --length 20 --count 10 --output creds.txt

Example Output

$ python3 pwgen.py --length 20 --require-all
7qF#vB2m!Kd8LzR@pTwX
Entropy: 131.1 bits  (strong)

$ python3 pwgen.py --passphrase --words 6
sonnet-drapery-unwired-hexagon-tundra-glimpse
Entropy: 77.5 bits  (strong)

$ python3 pwgen.py --length 6
kR2#mQ
Entropy: 39.3 bits  (weak - increase length to at least 16)

Error Handling

Condition Exception Response
Length below the class count ValueError Explain that the password cannot satisfy every class; exit 2
All character classes excluded ValueError Report that the alphabet is empty; exit 2
Wordlist file missing FileNotFoundError Report the path and exit 2
Wordlist too small ValueError Warn that entropy will be low and refuse below a threshold
Output file not writable PermissionError Report the path; never fall back to printing a secret unexpectedly
Negative or zero count ValueError Validate in argparse with a range check

Security Considerations

[!warning] Handle generated credentials as live secrets Anything this tool produces should be treated as a real password from the moment it is generated.

  • secrets, never random. random is a Mersenne Twister seeded from predictable state; observing a few outputs lets an attacker reconstruct the sequence. secrets draws from the OS CSPRNG.
  • Never log or print generated passwords to a shared terminal, and never write them into a report.
  • Do not accept or emit passwords via command-line arguments — the command line is visible to every user through ps and persists in shell history.
  • Set restrictive permissions (0600) on any output file, and prefer piping into a password manager over writing to disk.
  • Length beats composition. NIST SP 800-63B recommends long passphrases and a breach blocklist over forced symbol mixing.
  • Entropy is an upper bound. A high-entropy string that already appears in a breach corpus is worthless — check against a blocklist too.
  • Never reuse an example from documentation, including any shown here.

Testing

import math

import pwgen


def test_uses_secrets_not_random():
    source = open(pwgen.__file__, encoding="utf-8").read()
    assert "import secrets" in source
    assert "import random" not in source


def test_generated_passwords_are_unique():
    passwords = {pwgen.generate(20) for _ in range(1000)}
    assert len(passwords) == 1000        # collisions at this length are implausible


def test_length_is_respected():
    assert len(pwgen.generate(32)) == 32


def test_require_all_classes():
    pw = pwgen.generate(16, require_all=True)
    assert any(c.islower() for c in pw)
    assert any(c.isupper() for c in pw)
    assert any(c.isdigit() for c in pw)
    assert any(not c.isalnum() for c in pw)


def test_entropy_matches_formula():
    assert math.isclose(pwgen.entropy(20, 94), 20 * math.log2(94), rel_tol=1e-9)

Cover: uniqueness, length, class guarantees, entropy arithmetic, and rejection of an impossible configuration.

Extension Ideas

  • Pronounceable mode — generate syllable-based passwords for easier memorization.
  • Breach check — offline-check candidates against a local Have I Been Pwned k-anonymity range set.
  • Clipboard/zeroize — copy to clipboard and clear it after a timeout instead of printing.
  • Policy presets--preset nist / --preset pci bundles for common requirements.
  • Ambiguity filter — option to exclude look-alike characters (0/O, 1/l/I).

Troubleshooting

Symptom Likely cause Fix
Duplicate passwords across runs random used instead of secrets Switch to secrets; random is seeded deterministically
AttributeError: module 'secrets' has no attribute 'shuffle' secrets provides no shuffle Use secrets.SystemRandom().shuffle()
Password rejected by a target system Symbols outside the accepted set Use --no-symbols or a restricted alphabet
Entropy looks too high Alphabet size overstated Base it on the pool actually sampled from
Password mangled in the shell Special characters interpreted by the shell Quote in single quotes, or write to a file
Class guarantee weakens randomness Required characters placed at fixed positions Insert them, then shuffle the whole string

References

Related

  • [[Hash-Cracker]]
  • [[File-Integrity-Monitor]]
  • [[Hashlib-Module]]
  • [[Mini-Projects/Readme|Mini-Projects]] — module index
  • [[Readme|Python for Security Professionals]] — course home