diff --git a/src/password_manager/password_generation.py b/src/password_manager/password_generation.py index 8165822..1d32ddd 100644 --- a/src/password_manager/password_generation.py +++ b/src/password_manager/password_generation.py @@ -9,18 +9,18 @@ generated passwords meet complexity requirements. Ensure that all dependencies are installed and properly configured in your environment. -Never ever ever use or suggest to use Random Salt. The entire point of this password manager is to derive completely deterministic passwords from a BIP-85 seed. -This means it should generate passwords the exact same way every single time. Salts would break this functionality and is not appropriate for this softwares use case. +Never ever ever use Random Salt. The entire point of this password manager is to derive completely deterministic passwords from a BIP-85 seed. +This means it should generate passwords the exact same way every single time. Salts would break this functionality and is not appropriate for this software's use case. """ import os import logging import hashlib -import hmac import base64 import string import traceback from typing import Optional from termcolor import colored +import random from cryptography.hazmat.primitives.kdf.hkdf import HKDF from cryptography.hazmat.primitives import hashes @@ -113,8 +113,10 @@ class PasswordGenerator: Steps: 1. Derive entropy using BIP-85. 2. Use PBKDF2-HMAC-SHA256 to derive a key from entropy. - 3. Base64-encode the derived key and filter to allowed characters. + 3. Map the derived key to all allowed characters. 4. Ensure the password meets complexity requirements. + 5. Shuffle the password deterministically based on the derived key. + 6. Trim or extend the password to the desired length. Parameters: length (int): Desired length of the password. @@ -150,26 +152,30 @@ class PasswordGenerator: dk = hashlib.pbkdf2_hmac('sha256', entropy, b'', 100000) logger.debug(f"Derived key using PBKDF2: {dk.hex()}") - # Base64 encode the derived key - base64_password = base64.b64encode(dk).decode('utf-8') - logger.debug(f"Base64 encoded password: {base64_password}") - - # Filter to allowed characters - alphabet = string.ascii_letters + string.digits + string.punctuation - password = ''.join(filter(lambda x: x in alphabet, base64_password)) - logger.debug(f"Password after filtering: {password}") + # Map the derived key to all allowed characters + all_allowed = string.ascii_letters + string.digits + string.punctuation + password = ''.join(all_allowed[byte % len(all_allowed)] for byte in dk) + logger.debug(f"Password after mapping to all allowed characters: {password}") # Ensure the password meets complexity requirements - password = self.ensure_complexity(password, alphabet, dk) + password = self.ensure_complexity(password, all_allowed, dk) logger.debug(f"Password after ensuring complexity: {password}") + # Shuffle characters deterministically based on dk + shuffle_seed = int.from_bytes(dk, 'big') + rng = random.Random(shuffle_seed) + password_chars = list(password) + rng.shuffle(password_chars) + password = ''.join(password_chars) + logger.debug(f"Shuffled password deterministically.") + # Ensure password length if len(password) < length: # Extend the password deterministically while len(password) < length: dk = hashlib.pbkdf2_hmac('sha256', dk, b'', 1) - base64_extra = base64.b64encode(dk).decode('utf-8') - password += ''.join(filter(lambda x: x in alphabet, base64_extra)) + base64_extra = ''.join(all_allowed[byte % len(all_allowed)] for byte in dk) + password += ''.join(base64_extra) logger.debug(f"Extended password: {password}") password = password[:length] @@ -185,8 +191,9 @@ class PasswordGenerator: def ensure_complexity(self, password: str, alphabet: str, dk: bytes) -> str: """ - Ensures that the password contains at least one uppercase letter, one lowercase letter, - one digit, and one special character, modifying it deterministically if necessary. + Ensures that the password contains at least two uppercase letters, two lowercase letters, + two digits, and two special characters, modifying it deterministically if necessary. + Also balances the distribution of character types. Parameters: password (str): The initial password. @@ -204,11 +211,21 @@ class PasswordGenerator: password_chars = list(password) - has_upper = any(c in uppercase for c in password_chars) - has_lower = any(c in lowercase for c in password_chars) - has_digit = any(c in digits for c in password_chars) - has_special = any(c in special for c in password_chars) + # Current counts + current_upper = sum(1 for c in password_chars if c in uppercase) + current_lower = sum(1 for c in password_chars if c in lowercase) + current_digits = sum(1 for c in password_chars if c in digits) + current_special = sum(1 for c in password_chars if c in special) + logger.debug(f"Current character counts - Upper: {current_upper}, Lower: {current_lower}, Digits: {current_digits}, Special: {current_special}") + + # Set minimum counts + min_upper = 2 + min_lower = 2 + min_digits = 2 + min_special = 2 + + # Initialize derived key index dk_index = 0 dk_length = len(dk) @@ -218,29 +235,87 @@ class PasswordGenerator: dk_index += 1 return value - if not has_upper: - index = get_dk_value() % len(password_chars) - char = uppercase[get_dk_value() % len(uppercase)] - password_chars[index] = char - logger.debug(f"Added uppercase letter '{char}' at position {index}.") + # Replace characters to meet minimum counts + if current_upper < min_upper: + for _ in range(min_upper - current_upper): + index = get_dk_value() % len(password_chars) + char = uppercase[get_dk_value() % len(uppercase)] + password_chars[index] = char + logger.debug(f"Added uppercase letter '{char}' at position {index}.") - if not has_lower: - index = get_dk_value() % len(password_chars) - char = lowercase[get_dk_value() % len(lowercase)] - password_chars[index] = char - logger.debug(f"Added lowercase letter '{char}' at position {index}.") + if current_lower < min_lower: + for _ in range(min_lower - current_lower): + index = get_dk_value() % len(password_chars) + char = lowercase[get_dk_value() % len(lowercase)] + password_chars[index] = char + logger.debug(f"Added lowercase letter '{char}' at position {index}.") - if not has_digit: - index = get_dk_value() % len(password_chars) - char = digits[get_dk_value() % len(digits)] - password_chars[index] = char - logger.debug(f"Added digit '{char}' at position {index}.") + if current_digits < min_digits: + for _ in range(min_digits - current_digits): + index = get_dk_value() % len(password_chars) + char = digits[get_dk_value() % len(digits)] + password_chars[index] = char + logger.debug(f"Added digit '{char}' at position {index}.") - if not has_special: + if current_special < min_special: + for _ in range(min_special - current_special): + index = get_dk_value() % len(password_chars) + char = special[get_dk_value() % len(special)] + password_chars[index] = char + logger.debug(f"Added special character '{char}' at position {index}.") + + # Additional deterministic inclusion of symbols to increase score + symbol_target = 3 # Increase target number of symbols + current_symbols = sum(1 for c in password_chars if c in special) + additional_symbols_needed = max(symbol_target - current_symbols, 0) + + for _ in range(additional_symbols_needed): + if dk_index >= dk_length: + break # Avoid exceeding the derived key length index = get_dk_value() % len(password_chars) char = special[get_dk_value() % len(special)] password_chars[index] = char - logger.debug(f"Added special character '{char}' at position {index}.") + logger.debug(f"Added additional symbol '{char}' at position {index}.") + + # Ensure balanced distribution by assigning different character types to specific segments + # Example: Divide password into segments and assign different types + segment_length = len(password_chars) // 4 + if segment_length > 0: + for i, char_type in enumerate([uppercase, lowercase, digits, special]): + segment_start = i * segment_length + segment_end = segment_start + segment_length + if segment_end > len(password_chars): + segment_end = len(password_chars) + for j in range(segment_start, segment_end): + if i == 0 and password_chars[j] not in uppercase: + char = uppercase[get_dk_value() % len(uppercase)] + password_chars[j] = char + logger.debug(f"Assigned uppercase letter '{char}' to position {j}.") + elif i == 1 and password_chars[j] not in lowercase: + char = lowercase[get_dk_value() % len(lowercase)] + password_chars[j] = char + logger.debug(f"Assigned lowercase letter '{char}' to position {j}.") + elif i == 2 and password_chars[j] not in digits: + char = digits[get_dk_value() % len(digits)] + password_chars[j] = char + logger.debug(f"Assigned digit '{char}' to position {j}.") + elif i == 3 and password_chars[j] not in special: + char = special[get_dk_value() % len(special)] + password_chars[j] = char + logger.debug(f"Assigned special character '{char}' to position {j}.") + + # Shuffle again to distribute the characters more evenly + shuffle_seed = int.from_bytes(dk, 'big') + dk_index # Modify seed to vary shuffle + rng = random.Random(shuffle_seed) + rng.shuffle(password_chars) + logger.debug(f"Shuffled password characters for balanced distribution.") + + # Final counts after modifications + final_upper = sum(1 for c in password_chars if c in uppercase) + final_lower = sum(1 for c in password_chars if c in lowercase) + final_digits = sum(1 for c in password_chars if c in digits) + final_special = sum(1 for c in password_chars if c in special) + logger.debug(f"Final character counts - Upper: {final_upper}, Lower: {final_lower}, Digits: {final_digits}, Special: {final_special}") return ''.join(password_chars) @@ -249,4 +324,3 @@ class PasswordGenerator: logger.error(traceback.format_exc()) # Log full traceback print(colored(f"Error: Failed to ensure password complexity: {e}", 'red')) raise -