mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
Refactor password generation with helpers
This commit is contained in:
@@ -73,6 +73,41 @@ class PasswordGenerator:
|
||||
print(colored(f"Error: Failed to initialize PasswordGenerator: {e}", "red"))
|
||||
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(
|
||||
self, length: int = DEFAULT_PASSWORD_LENGTH, index: int = 0
|
||||
) -> str:
|
||||
@@ -111,52 +146,20 @@ class PasswordGenerator:
|
||||
f"Password length must not exceed {MAX_PASSWORD_LENGTH} characters."
|
||||
)
|
||||
|
||||
# Derive entropy using BIP-85
|
||||
entropy = self.bip85.derive_entropy(index=index, bytes_len=64, app_no=32)
|
||||
logger.debug(f"Derived entropy: {entropy.hex()}")
|
||||
dk = self._derive_password_entropy(index=index)
|
||||
|
||||
# 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
|
||||
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, 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.")
|
||||
password = self._map_entropy_to_chars(dk, all_allowed)
|
||||
password = self._enforce_complexity(password, all_allowed, dk)
|
||||
password = self._shuffle_deterministically(password, dk)
|
||||
|
||||
# Ensure password length by extending if necessary
|
||||
if len(password) < length:
|
||||
while len(password) < length:
|
||||
dk = hashlib.pbkdf2_hmac("sha256", dk, b"", 1)
|
||||
base64_extra = "".join(
|
||||
all_allowed[byte % len(all_allowed)] for byte in dk
|
||||
)
|
||||
password += "".join(base64_extra)
|
||||
extra = self._map_entropy_to_chars(dk, all_allowed)
|
||||
password += extra
|
||||
password = self._shuffle_deterministically(password, dk)
|
||||
logger.debug(f"Extended password: {password}")
|
||||
|
||||
# Trim the password to the desired length
|
||||
@@ -171,7 +174,7 @@ class PasswordGenerator:
|
||||
print(colored(f"Error: Failed to generate password: {e}", "red"))
|
||||
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,
|
||||
two digits, and two special characters, modifying it deterministically if necessary.
|
||||
|
55
src/tests/test_password_helpers.py
Normal file
55
src/tests/test_password_helpers.py
Normal 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
|
Reference in New Issue
Block a user