Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,6 @@ projects/**/downloads/
# Environment variables
.env
.env.local

.superpowers
docs/superpowers
81 changes: 76 additions & 5 deletions projects/beginner/password_generator/README.md
Original file line number Diff line number Diff line change
@@ -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

<!-- Add a terminal screenshot of the CLI generator in action -->
`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.
107 changes: 107 additions & 0 deletions projects/beginner/password_generator/password_generator.py
Original file line number Diff line number Diff line change
@@ -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)
52 changes: 52 additions & 0 deletions projects/beginner/password_generator/test_password_generator.py
Original file line number Diff line number Diff line change
@@ -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
Loading