mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
Remove duplicated code in key derivation
This commit is contained in:
@@ -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
|
||||
|
Reference in New Issue
Block a user