diff --git a/.gitignore b/.gitignore index 4d7811e..bb675c3 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ projects/**/downloads/ # Environment variables .env .env.local + +.superpowers +docs/superpowers diff --git a/projects/beginner/password_generator/README.md b/projects/beginner/password_generator/README.md index cbf761d..61fd445 100644 --- a/projects/beginner/password_generator/README.md +++ b/projects/beginner/password_generator/README.md @@ -1,23 +1,94 @@ # Password Generator -> 🚧 **Status: Planned.** This project's folder is scaffolded; full implementation, tests, and README are coming in a follow-up pass — see the two completed reference projects: [Calculator](../../beginner/calculator) and [Number Guessing Game](../../beginner/number_guessing_game) for the quality bar and structure being followed. +A cryptographically secure command-line password generator built with Python's `secrets` module (CSPRNG). Supports customizable character sets, ambiguous character filtering, guaranteed set representation, command-line arguments, interactive mode, and a complete pytest suite. -## Planned Features +## Features -_To be defined when this project is built._ +- **Cryptographically Secure**: Built using `secrets` (CSPRNG) instead of standard pseudo-random `random` for high entropy and security. +- **Guaranteed Character Representation**: Guarantees that at least one character from each enabled character set (letters, digits, symbols) is included in the output. +- **Customizable Character Sets**: Toggle letters, digits, or punctuation symbols on demand. +- **Ambiguous Character Filtering**: Optionally exclude confusing characters (`1`, `l`, `I`, `0`, `O`). +- **Flexible Modes**: + - **CLI Flags Mode**: Fast password generation via flags (`-l 16 --no-symbols`). + - **Interactive Mode**: Guided prompt interface (`python password_generator.py`). +- **Unit Tested**: Full test coverage with `pytest`. ## Requirements -_To be defined._ +``` +pytest>=8.2.0 +``` + +Install repository requirements: +`pip install -r ../../../requirements.txt` ## Usage -_To be defined._ +### 1. Interactive Mode +Run without arguments to start the interactive prompt: + +```bash +python password_generator.py +``` + +```text +=== Password Generator === +Enter password length (default 12): 16 +Include letters? [Y/n]: y +Include numbers? [Y/n]: y +Include symbols? [Y/n]: y +Exclude ambiguous characters (1, l, I, 0, O)? [y/N]: y + +Generated Password: k8#P9$mQ2!vX7&wZ +``` + +### 2. Command Line Flags +Generate passwords directly from the terminal with custom flags: + +```bash +# Default 12-character password +python password_generator.py +# Example output: aB3$k9#mP2!q + +# 24-character password without symbols +python password_generator.py -l 24 --no-symbols + +# 16-character password excluding ambiguous characters (1, l, I, 0, O) +python password_generator.py -l 16 --exclude-ambiguous +``` + +## Running Tests + +Run the `pytest` test suite: + +```bash +python -m pytest test_password_generator.py -v +``` ## Project Structure ``` password_generator/ ├── README.md +├── password_generator.py # Core generator, argparse CLI, and interactive interface +├── test_password_generator.py # pytest test suite └── screenshots/ ``` + +## Screenshots + + +`screenshots/cli_demo.png` + +## How It Works + +1. **CSPRNG Randomness**: Uses `secrets.choice()` backed by `/dev/urandom` or OS entropy sources (rather than pseudo-random `random.choice()`) to prevent password predictability. +2. **Guaranteed Distribution**: Pre-selects 1 character from each enabled category (e.g. 1 letter, 1 digit, 1 symbol) to ensure no set is omitted by chance. +3. **Secure Shuffling**: Fills remaining slots from the combined pool and shuffles the character array using `secrets.SystemRandom().shuffle()`. + +## Possible Extensions + +- Add Diceware passphrase generation (multi-word passwords). +- Integrate password strength/entropy calculation (bits of entropy). +- Add automatic clipboard copy support via `pyperclip`. +- Build a lightweight Tkinter or web GUI. diff --git a/projects/beginner/password_generator/password_generator.py b/projects/beginner/password_generator/password_generator.py new file mode 100644 index 0000000..2b70523 --- /dev/null +++ b/projects/beginner/password_generator/password_generator.py @@ -0,0 +1,107 @@ +import argparse +import secrets +import string +import sys + +AMBIGUOUS_CHARS = "1lI0O" + +def generate_password( + length: int = 12, + include_letters: bool = True, + include_digits: bool = True, + include_symbols: bool = True, + exclude_ambiguous: bool = False +) -> str: + """ + Generate a cryptographically secure random password. + """ + pools = [] + if include_letters: + pool = string.ascii_letters + if exclude_ambiguous: + pool = "".join(c for c in pool if c not in AMBIGUOUS_CHARS) + pools.append(pool) + + if include_digits: + pool = string.digits + if exclude_ambiguous: + pool = "".join(c for c in pool if c not in AMBIGUOUS_CHARS) + pools.append(pool) + + if include_symbols: + pool = string.punctuation + if exclude_ambiguous: + pool = "".join(c for c in pool if c not in AMBIGUOUS_CHARS) + pools.append(pool) + + if not pools: + raise ValueError("At least one character set must be enabled.") + + min_length = len(pools) + if length < min_length: + raise ValueError(f"Password length must be at least {min_length} for the selected character sets.") + + password_chars = [secrets.choice(pool) for pool in pools] + + combined_pool = "".join(pools) + remaining_length = length - len(password_chars) + for _ in range(remaining_length): + password_chars.append(secrets.choice(combined_pool)) + + secrets.SystemRandom().shuffle(password_chars) + return "".join(password_chars) + +def parse_args(args=None): + parser = argparse.ArgumentParser(description="Cryptographically Secure Password Generator CLI") + parser.add_argument("-l", "--length", type=int, default=12, help="Password length (default: 12)") + parser.add_argument("--no-letters", action="store_false", dest="include_letters", help="Exclude letters") + parser.add_argument("--no-digits", action="store_false", dest="include_digits", help="Exclude numbers/digits") + parser.add_argument("--no-symbols", action="store_false", dest="include_symbols", help="Exclude special symbols") + parser.add_argument("--exclude-ambiguous", action="store_true", help="Exclude ambiguous characters (1, l, I, 0, O)") + parser.add_argument("-i", "--interactive", action="store_true", help="Run interactive prompt mode") + return parser.parse_args(args) + +def interactive_mode(): + print("=== Password Generator ===") + try: + raw_len = input("Enter password length (default 12): ").strip() + length = int(raw_len) if raw_len else 12 + + inc_letters = input("Include letters? [Y/n]: ").strip().lower() != 'n' + inc_digits = input("Include numbers? [Y/n]: ").strip().lower() != 'n' + inc_symbols = input("Include symbols? [Y/n]: ").strip().lower() != 'n' + exc_ambig = input("Exclude ambiguous characters (1, l, I, 0, O)? [y/N]: ").strip().lower() == 'y' + + pwd = generate_password( + length=length, + include_letters=inc_letters, + include_digits=inc_digits, + include_symbols=inc_symbols, + exclude_ambiguous=exc_ambig + ) + print(f"\nGenerated Password: {pwd}") + except ValueError as err: + print(f"Error: {err}") + sys.exit(1) + +if __name__ == "__main__": + if len(sys.argv) == 1: + args = parse_args() + interactive_mode() + else: + args = parse_args() + if args.interactive: + interactive_mode() + else: + try: + pwd = generate_password( + length=args.length, + include_letters=args.include_letters, + include_digits=args.include_digits, + include_symbols=args.include_symbols, + exclude_ambiguous=args.exclude_ambiguous + ) + print(pwd) + except ValueError as err: + print(f"Error: {err}", file=sys.stderr) + sys.exit(1) \ No newline at end of file diff --git a/projects/beginner/password_generator/test_password_generator.py b/projects/beginner/password_generator/test_password_generator.py new file mode 100644 index 0000000..42c21de --- /dev/null +++ b/projects/beginner/password_generator/test_password_generator.py @@ -0,0 +1,52 @@ +import pytest +import string +from password_generator import generate_password, parse_args + +def test_default_password_generation(): + password = generate_password() + assert len(password) == 12 + assert isinstance(password, str) + +def test_custom_length(): + for length in [4, 16, 64]: + password = generate_password(length=length) + assert len(password) == length + +def test_character_set_guarantee(): + for _ in range(10): + password = generate_password(length=12, include_letters=True, include_digits=True, include_symbols=True) + assert any(c in string.ascii_letters for c in password) + assert any(c in string.digits for c in password) + assert any(c in string.punctuation for c in password) + +def test_exclude_digits_and_symbols(): + password = generate_password(length=20, include_letters=True, include_digits=False, include_symbols=False) + assert len(password) == 20 + assert all(c in string.ascii_letters for c in password) + +def test_exclude_ambiguous_characters(): + ambiguous = set("1lI0O") + for _ in range(20): + password = generate_password(length=30, exclude_ambiguous=True) + assert not any(c in ambiguous for c in password) + +def test_invalid_length_or_empty_sets(): + with pytest.raises(ValueError, match="At least one character set must be enabled"): + generate_password(include_letters=False, include_digits=False, include_symbols=False) + + with pytest.raises(ValueError, match="Password length must be at least"): + generate_password(length=2, include_letters=True, include_digits=True, include_symbols=True) + +def test_parse_args_defaults(): + args = parse_args([]) + assert args.length == 12 + assert args.include_letters is True + assert args.include_digits is True + assert args.include_symbols is True + assert args.exclude_ambiguous is False + +def test_parse_args_custom(): + args = parse_args(["-l", "24", "--no-symbols", "--exclude-ambiguous"]) + assert args.length == 24 + assert args.include_symbols is False + assert args.exclude_ambiguous is True