PYTHONPython

generator

real world projects / password manager / pwm

PYTHON
generator.py🐍
"""
Password generation utilities.
"""

import secrets
import string
from typing import List

# Character sets
LOWERCASE = string.ascii_lowercase
UPPERCASE = string.ascii_uppercase
DIGITS = string.digits
SYMBOLS = "!@#$%^&*()_+-=[]{}|;:,.<>?"
AMBIGUOUS = "l1I0O"  # Characters that look similar


def generate_password(
    length: int = 16,
    lowercase: bool = True,
    uppercase: bool = True,
    digits: bool = True,
    symbols: bool = True,
    exclude_ambiguous: bool = False,
    exclude_chars: str = ""
) -> str:
    """
    Generate a cryptographically secure random password.
    
    Args:
        length: Password length (minimum 4)
        lowercase: Include lowercase letters
        uppercase: Include uppercase letters
        digits: Include digits
        symbols: Include special characters
        exclude_ambiguous: Exclude similar-looking characters
        exclude_chars: Additional characters to exclude
    
    Returns:
        Random password string
    
    Raises:
        ValueError: If no character types enabled or length too short
    """
    if length < 4:
        raise ValueError("Password length must be at least 4")
    
    # Build character pool
    pool = ""
    required = []  # Ensure at least one of each type
    
    if lowercase:
        chars = LOWERCASE
        if exclude_ambiguous:
            chars = ''.join(c for c in chars if c not in AMBIGUOUS)
        pool += chars
        required.append(chars)
    
    if uppercase:
        chars = UPPERCASE
        if exclude_ambiguous:
            chars = ''.join(c for c in chars if c not in AMBIGUOUS)
        pool += chars
        required.append(chars)
    
    if digits:
        chars = DIGITS
        if exclude_ambiguous:
            chars = ''.join(c for c in chars if c not in AMBIGUOUS)
        pool += chars
        required.append(chars)
    
    if symbols:
        pool += SYMBOLS
        required.append(SYMBOLS)
    
    # Remove excluded characters
    if exclude_chars:
        pool = ''.join(c for c in pool if c not in exclude_chars)
    
    if not pool:
        raise ValueError("No characters available for password generation")
    
    # Generate password ensuring at least one of each required type
    password = []
    
    # Add one of each required type
    for chars in required:
        valid_chars = ''.join(c for c in chars if c not in exclude_chars)
        if valid_chars:
            password.append(secrets.choice(valid_chars))
    
    # Fill remaining length
    remaining = length - len(password)
    password.extend(secrets.choice(pool) for _ in range(remaining))
    
    # Shuffle to randomize positions
    password_list = list(password)
    secrets.SystemRandom().shuffle(password_list)
    
    return ''.join(password_list)


def generate_passphrase(
    words: int = 4,
    separator: str = "-",
    capitalize: bool = False,
    include_number: bool = False
) -> str:
    """
    Generate a random passphrase using common words.
    
    Args:
        words: Number of words
        separator: Word separator
        capitalize: Capitalize first letter of each word
        include_number: Add a random number
    
    Returns:
        Random passphrase
    """
    # Common words (subset of EFF word list)
    wordlist = [
        "apple", "banana", "cherry", "dragon", "eagle", "falcon", "guitar",
        "hammer", "island", "jungle", "kitten", "lemon", "marble", "needle",
        "orange", "pencil", "quartz", "rabbit", "silver", "tiger", "umbrella",
        "violet", "wallet", "yellow", "zebra", "anchor", "beacon", "castle",
        "desert", "empire", "forest", "galaxy", "harbor", "impact", "jacket",
        "knight", "ladder", "magnet", "nectar", "oasis", "palace", "quantum",
        "rhythm", "sunset", "temple", "upward", "velvet", "window", "zodiac",
        "arctic", "bronze", "cosmic", "domain", "engine", "frozen", "global",
        "hollow", "ignore", "joyful", "kernel", "legend", "mellow", "noble",
        "octave", "planet", "rocket", "shadow", "throne", "unique", "voyage"
    ]
    
    # Select random words
    selected = [secrets.choice(wordlist) for _ in range(words)]
    
    # Apply transformations
    if capitalize:
        selected = [w.capitalize() for w in selected]
    
    passphrase = separator.join(selected)
    
    if include_number:
        passphrase += separator + str(secrets.randbelow(1000))
    
    return passphrase


def calculate_password_strength(password: str) -> dict:
    """
    Calculate password strength score.
    
    Args:
        password: Password to analyze
    
    Returns:
        Dictionary with score and details
    """
    score = 0
    feedback = []
    
    length = len(password)
    
    # Length scoring
    if length < 8:
        feedback.append("Password is too short (minimum 8 characters)")
    elif length < 12:
        score += 20
        feedback.append("Consider using a longer password")
    elif length < 16:
        score += 30
    else:
        score += 40
    
    # Character variety
    has_lower = any(c in LOWERCASE for c in password)
    has_upper = any(c in UPPERCASE for c in password)
    has_digit = any(c in DIGITS for c in password)
    has_symbol = any(c in SYMBOLS for c in password)
    
    variety_count = sum([has_lower, has_upper, has_digit, has_symbol])
    
    if variety_count == 1:
        feedback.append("Add different character types")
    elif variety_count == 2:
        score += 15
        feedback.append("Consider adding more character types")
    elif variety_count == 3:
        score += 25
    else:
        score += 35
    
    # Common patterns to avoid
    common_patterns = [
        "123", "abc", "qwerty", "password", "letmein",
        "111", "000", "aaa", "admin", "login"
    ]
    
    password_lower = password.lower()
    for pattern in common_patterns:
        if pattern in password_lower:
            score -= 20
            feedback.append(f"Avoid common pattern: '{pattern}'")
            break
    
    # Sequential characters
    sequential = 0
    for i in range(len(password) - 1):
        if ord(password[i+1]) == ord(password[i]) + 1:
            sequential += 1
    
    if sequential >= 3:
        score -= 10
        feedback.append("Avoid sequential characters")
    
    # Repeated characters
    repeated = 0
    for i in range(len(password) - 1):
        if password[i] == password[i+1]:
            repeated += 1
    
    if repeated >= 3:
        score -= 10
        feedback.append("Avoid repeated characters")
    
    # Ensure score is in range
    score = max(0, min(100, score))
    
    # Determine strength level
    if score < 40:
        level = "Weak"
    elif score < 60:
        level = "Fair"
    elif score < 80:
        level = "Good"
    else:
        level = "Strong"
    
    return {
        "score": score,
        "level": level,
        "feedback": feedback,
        "length": length,
        "has_lowercase": has_lower,
        "has_uppercase": has_upper,
        "has_digits": has_digit,
        "has_symbols": has_symbol
    }


def check_password_breach(password: str) -> bool:
    """
    Check if password appears in known data breaches.
    Uses k-anonymity model (only first 5 chars of hash sent).
    
    Note: This is a placeholder - real implementation would
    use the HaveIBeenPwned API.
    
    Args:
        password: Password to check
    
    Returns:
        True if password found in breaches
    """
    import hashlib
    
    # In a real implementation, you would:
    # 1. SHA1 hash the password
    # 2. Send first 5 characters to HIBP API
    # 3. Check if remaining hash is in response
    
    # Placeholder - always returns False
    return False
PreviousNext