Refactor password generation with helpers

This commit is contained in:
thePR0M3TH3AN
2025-06-30 14:11:16 -04:00
parent 1e41368f80
commit 57fde0139f
2 changed files with 98 additions and 40 deletions

View File

@@ -73,6 +73,41 @@ class PasswordGenerator:
print(colored(f"Error: Failed to initialize PasswordGenerator: {e}", "red")) print(colored(f"Error: Failed to initialize PasswordGenerator: {e}", "red"))
raise raise
def _derive_password_entropy(self, index: int) -> bytes:
"""Derive deterministic entropy for password generation."""
entropy = self.bip85.derive_entropy(index=index, bytes_len=64, app_no=32)
logger.debug(f"Derived entropy: {entropy.hex()}")
hkdf = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=None,
info=b"password-generation",
backend=default_backend(),
)
hkdf_derived = hkdf.derive(entropy)
logger.debug(f"Derived key using HKDF: {hkdf_derived.hex()}")
dk = hashlib.pbkdf2_hmac("sha256", entropy, b"", 100000)
logger.debug(f"Derived key using PBKDF2: {dk.hex()}")
return dk
def _map_entropy_to_chars(self, dk: bytes, alphabet: str) -> str:
"""Map derived bytes to characters from the provided alphabet."""
password = "".join(alphabet[byte % len(alphabet)] for byte in dk)
logger.debug(f"Password after mapping to all allowed characters: {password}")
return password
def _shuffle_deterministically(self, password: str, dk: bytes) -> str:
"""Deterministically shuffle characters using derived bytes."""
shuffle_seed = int.from_bytes(dk, "big")
rng = random.Random(shuffle_seed)
password_chars = list(password)
rng.shuffle(password_chars)
shuffled = "".join(password_chars)
logger.debug("Shuffled password deterministically.")
return shuffled
def generate_password( def generate_password(
self, length: int = DEFAULT_PASSWORD_LENGTH, index: int = 0 self, length: int = DEFAULT_PASSWORD_LENGTH, index: int = 0
) -> str: ) -> str:
@@ -111,52 +146,20 @@ class PasswordGenerator:
f"Password length must not exceed {MAX_PASSWORD_LENGTH} characters." f"Password length must not exceed {MAX_PASSWORD_LENGTH} characters."
) )
# Derive entropy using BIP-85 dk = self._derive_password_entropy(index=index)
entropy = self.bip85.derive_entropy(index=index, bytes_len=64, app_no=32)
logger.debug(f"Derived entropy: {entropy.hex()}")
# Use HKDF to derive key from entropy
hkdf = HKDF(
algorithm=hashes.SHA256(),
length=32, # 256 bits for AES-256
salt=None,
info=b"password-generation",
backend=default_backend(),
)
derived_key = hkdf.derive(entropy)
logger.debug(f"Derived key using HKDF: {derived_key.hex()}")
# Use PBKDF2-HMAC-SHA256 to derive a key from entropy
dk = hashlib.pbkdf2_hmac("sha256", entropy, b"", 100000)
logger.debug(f"Derived key using PBKDF2: {dk.hex()}")
# Map the derived key to all allowed characters
all_allowed = string.ascii_letters + string.digits + string.punctuation all_allowed = string.ascii_letters + string.digits + string.punctuation
password = "".join(all_allowed[byte % len(all_allowed)] for byte in dk) password = self._map_entropy_to_chars(dk, all_allowed)
logger.debug( password = self._enforce_complexity(password, all_allowed, dk)
f"Password after mapping to all allowed characters: {password}" password = self._shuffle_deterministically(password, dk)
)
# Ensure the password meets complexity requirements
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("Shuffled password deterministically.")
# Ensure password length by extending if necessary # Ensure password length by extending if necessary
if len(password) < length: if len(password) < length:
while len(password) < length: while len(password) < length:
dk = hashlib.pbkdf2_hmac("sha256", dk, b"", 1) dk = hashlib.pbkdf2_hmac("sha256", dk, b"", 1)
base64_extra = "".join( extra = self._map_entropy_to_chars(dk, all_allowed)
all_allowed[byte % len(all_allowed)] for byte in dk password += extra
) password = self._shuffle_deterministically(password, dk)
password += "".join(base64_extra)
logger.debug(f"Extended password: {password}") logger.debug(f"Extended password: {password}")
# Trim the password to the desired length # Trim the password to the desired length
@@ -171,7 +174,7 @@ class PasswordGenerator:
print(colored(f"Error: Failed to generate password: {e}", "red")) print(colored(f"Error: Failed to generate password: {e}", "red"))
raise raise
def ensure_complexity(self, password: str, alphabet: str, dk: bytes) -> str: def _enforce_complexity(self, password: str, alphabet: str, dk: bytes) -> str:
""" """
Ensures that the password contains at least two uppercase letters, two lowercase letters, Ensures that the password contains at least two uppercase letters, two lowercase letters,
two digits, and two special characters, modifying it deterministically if necessary. two digits, and two special characters, modifying it deterministically if necessary.

View File

@@ -0,0 +1,55 @@
import string
from password_manager.password_generation import PasswordGenerator
class DummyEnc:
def derive_seed_from_mnemonic(self, mnemonic):
return b"\x00" * 32
class DummyBIP85:
def derive_entropy(self, index: int, bytes_len: int, app_no: int = 32) -> bytes:
return bytes((index + i) % 256 for i in range(bytes_len))
def make_generator():
pg = PasswordGenerator.__new__(PasswordGenerator)
pg.encryption_manager = DummyEnc()
pg.bip85 = DummyBIP85()
return pg
def test_derive_password_entropy_length():
pg = make_generator()
dk = pg._derive_password_entropy(index=1)
assert isinstance(dk, bytes)
assert len(dk) == 32
dk2 = pg._derive_password_entropy(index=2)
assert dk != dk2
def test_map_entropy_to_chars_only_uses_alphabet():
pg = make_generator()
alphabet = string.ascii_letters + string.digits
mapped = pg._map_entropy_to_chars(b"\x00\x01\x02", alphabet)
assert all(c in alphabet for c in mapped)
assert len(mapped) == 3
def test_enforce_complexity_minimum_counts():
pg = make_generator()
alphabet = string.ascii_letters + string.digits + string.punctuation
dk = bytes(range(32))
result = pg._enforce_complexity("a" * 32, alphabet, dk)
assert sum(1 for c in result if c.isupper()) >= 2
assert sum(1 for c in result if c.islower()) >= 2
assert sum(1 for c in result if c.isdigit()) >= 2
assert sum(1 for c in result if c in string.punctuation) >= 2
def test_shuffle_deterministically_repeatable():
pg = make_generator()
dk = bytes(range(32))
pw1 = pg._shuffle_deterministically("abcdef", dk)
pw2 = pg._shuffle_deterministically("abcdef", dk)
assert pw1 == pw2