Merge pull request #12 from PR0M3TH3AN/codex/refactor-key_derivation.py-to-remove-duplicates

Fix duplicated module in key derivation utility
This commit is contained in:
thePR0M3TH3AN
2025-06-29 12:12:52 -04:00
committed by GitHub

View File

@@ -3,8 +3,8 @@
"""
Key Derivation Module
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 software's use case.
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 software's use case.
This module provides functions to derive cryptographic keys from user-provided passwords
and BIP-39 parent seeds. The derived keys are compatible with Fernet for symmetric encryption
@@ -31,163 +31,6 @@ from cryptography.hazmat.backends import default_backend
# Instantiate the logger
logger = logging.getLogger(__name__)
def derive_key_from_password(password: str, iterations: int = 100_000) -> bytes:
"""
Derives a Fernet-compatible encryption key from the provided password using PBKDF2-HMAC-SHA256.
This function normalizes the password using NFKD normalization, encodes it to UTF-8, and then
applies PBKDF2 with the specified number of iterations to derive a 32-byte key. The derived key
is then URL-safe base64-encoded to ensure compatibility with Fernet.
Parameters:
password (str): The user's password.
iterations (int, optional): Number of iterations for the PBKDF2 algorithm. Defaults to 100,000.
Returns:
bytes: A URL-safe base64-encoded encryption key suitable for Fernet.
Raises:
ValueError: If the password is empty or too short.
"""
if not password:
logger.error("Password cannot be empty.")
raise ValueError("Password cannot be empty.")
if len(password) < 8:
logger.warning("Password length is less than recommended (8 characters).")
# Normalize the password to NFKD form and encode to UTF-8
normalized_password = unicodedata.normalize('NFKD', password).strip()
password_bytes = normalized_password.encode('utf-8')
try:
# Derive the key using PBKDF2-HMAC-SHA256
logger.debug("Starting key derivation from password.")
key = hashlib.pbkdf2_hmac(
hash_name='sha256',
password=password_bytes,
salt=b'', # No salt for deterministic key derivation
iterations=iterations,
dklen=32 # 256-bit key for Fernet
)
logger.debug(f"Derived key (hex): {key.hex()}")
# Encode the key in URL-safe base64
key_b64 = base64.urlsafe_b64encode(key)
logger.debug(f"Base64-encoded key: {key_b64.decode()}")
return key_b64
except Exception as e:
logger.error(f"Error deriving key from password: {e}")
logger.error(traceback.format_exc()) # Log full traceback
raise
def derive_key_from_parent_seed(parent_seed: str, fingerprint: str = None) -> bytes:
"""
Derives a 32-byte cryptographic key from a BIP-39 parent seed using HKDF.
Optionally, include a fingerprint to differentiate key derivation per fingerprint.
:param parent_seed: The 12-word BIP-39 seed phrase.
:param fingerprint: An optional fingerprint to create unique keys per fingerprint.
:return: A 32-byte derived key.
"""
try:
# Generate seed bytes from mnemonic
seed = Bip39SeedGenerator(parent_seed).Generate()
# If a fingerprint is provided, use it to differentiate the derivation
if fingerprint:
# Convert fingerprint to a stable integer index
index = int(hashlib.sha256(fingerprint.encode()).hexdigest(), 16) % (2**31)
info = f'password-manager-{index}'.encode() # Unique info for HKDF
else:
info = b'password-manager'
# Derive key using HKDF
hkdf = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=None, # No salt for deterministic derivation
info=info,
backend=default_backend()
)
derived_key = hkdf.derive(seed)
if len(derived_key) != 32:
raise ValueError(f"Derived key length is {len(derived_key)} bytes; expected 32 bytes.")
return derived_key
except Exception as e:
logger.error(f"Failed to derive key using HKDF: {e}")
logger.error(traceback.format_exc())
raise
class KeyManager:
def __init__(self, parent_seed: str, fingerprint: str = None):
self.parent_seed = parent_seed
self.fingerprint = fingerprint
self.bip85 = self.initialize_bip85()
self.keys = self.generate_nostr_keys()
def initialize_bip85(self):
seed_bytes = Bip39SeedGenerator(self.parent_seed).Generate()
bip85 = BIP85(seed_bytes)
return bip85
def generate_nostr_keys(self) -> Keys:
"""
Derives a unique Nostr key pair for the given fingerprint using BIP-85.
:return: An instance of Keys containing the Nostr key pair.
"""
# Use a derivation path that includes the fingerprint
# Convert fingerprint to an integer index (e.g., using a hash function)
index = int(hashlib.sha256(self.fingerprint.encode()).hexdigest(), 16) % (2**31) if self.fingerprint else 0
# Derive entropy for Nostr key (32 bytes)
entropy_bytes = self.bip85.derive_entropy(
app=BIP85.Applications.ENTROPY,
index=index,
size=32
)
# Generate Nostr key pair from entropy
private_key_hex = entropy_bytes.hex()
keys = Keys(priv_key=private_key_hex)
return keys
# utils/key_derivation.py
"""
Key Derivation Module
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 software's use case.
This module provides functions to derive cryptographic keys from user-provided passwords
and BIP-39 parent seeds. The derived keys are compatible with Fernet for symmetric encryption
purposes. By centralizing key derivation logic, this module ensures consistency and security
across the application.
Ensure that all dependencies are installed and properly configured in your environment.
"""
import os
import hashlib
import base64
import unicodedata
import logging
import traceback
from typing import Union
from bip_utils import Bip39SeedGenerator
from local_bip85.bip85 import BIP85
from monstr.encrypt import Keys
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
# Instantiate the logger
logger = logging.getLogger(__name__)
def derive_key_from_password(password: str, iterations: int = 100_000) -> bytes:
"""
@@ -213,20 +56,20 @@ def derive_key_from_password(password: str, iterations: int = 100_000) -> bytes:
if len(password) < 8:
logger.warning("Password length is less than recommended (8 characters).")
# Normalize the password to NFKD form and encode to UTF-8
normalized_password = unicodedata.normalize('NFKD', password).strip()
password_bytes = normalized_password.encode('utf-8')
normalized_password = unicodedata.normalize("NFKD", password).strip()
password_bytes = normalized_password.encode("utf-8")
try:
# Derive the key using PBKDF2-HMAC-SHA256
logger.debug("Starting key derivation from password.")
key = hashlib.pbkdf2_hmac(
hash_name='sha256',
hash_name="sha256",
password=password_bytes,
salt=b'', # No salt for deterministic key derivation
salt=b"", # No salt for deterministic key derivation
iterations=iterations,
dklen=32 # 256-bit key for Fernet
dklen=32, # 256-bit key for Fernet
)
logger.debug(f"Derived key (hex): {key.hex()}")
@@ -241,6 +84,7 @@ def derive_key_from_password(password: str, iterations: int = 100_000) -> bytes:
logger.error(traceback.format_exc()) # Log full traceback
raise
def derive_key_from_parent_seed(parent_seed: str, fingerprint: str = None) -> bytes:
"""
Derives a 32-byte cryptographic key from a BIP-39 parent seed using HKDF.
@@ -253,34 +97,37 @@ def derive_key_from_parent_seed(parent_seed: str, fingerprint: str = None) -> by
try:
# Generate seed bytes from mnemonic
seed = Bip39SeedGenerator(parent_seed).Generate()
# If a fingerprint is provided, use it to differentiate the derivation
if fingerprint:
# Convert fingerprint to a stable integer index
index = int(hashlib.sha256(fingerprint.encode()).hexdigest(), 16) % (2**31)
info = f'password-manager-{index}'.encode() # Unique info for HKDF
info = f"password-manager-{index}".encode() # Unique info for HKDF
else:
info = b'password-manager'
info = b"password-manager"
# Derive key using HKDF
hkdf = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=None, # No salt for deterministic derivation
info=info,
backend=default_backend()
backend=default_backend(),
)
derived_key = hkdf.derive(seed)
if len(derived_key) != 32:
raise ValueError(f"Derived key length is {len(derived_key)} bytes; expected 32 bytes.")
raise ValueError(
f"Derived key length is {len(derived_key)} bytes; expected 32 bytes."
)
return derived_key
except Exception as e:
logger.error(f"Failed to derive key using HKDF: {e}")
logger.error(traceback.format_exc())
raise
class KeyManager:
def __init__(self, parent_seed: str, fingerprint: str = None):
self.parent_seed = parent_seed
@@ -301,13 +148,15 @@ class KeyManager:
"""
# Use a derivation path that includes the fingerprint
# Convert fingerprint to an integer index (e.g., using a hash function)
index = int(hashlib.sha256(self.fingerprint.encode()).hexdigest(), 16) % (2**31) if self.fingerprint else 0
index = (
int(hashlib.sha256(self.fingerprint.encode()).hexdigest(), 16) % (2**31)
if self.fingerprint
else 0
)
# Derive entropy for Nostr key (32 bytes)
entropy_bytes = self.bip85.derive_entropy(
app=BIP85.Applications.ENTROPY,
index=index,
size=32
app=BIP85.Applications.ENTROPY, index=index, size=32
)
# Generate Nostr key pair from entropy