mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-09 15:58:48 +00:00
update
This commit is contained in:
24
src/utils/__init__.py
Normal file
24
src/utils/__init__.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# utils/__init__.py
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
try:
|
||||
from .file_lock import lock_file
|
||||
from .key_derivation import derive_key_from_password, derive_key_from_parent_seed
|
||||
from .checksum import calculate_checksum, verify_checksum
|
||||
from .password_prompt import prompt_for_password
|
||||
|
||||
logging.info("Modules imported successfully.")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to import one or more modules: {e}")
|
||||
logging.error(traceback.format_exc()) # Log full traceback
|
||||
|
||||
__all__ = [
|
||||
'derive_key_from_password',
|
||||
'derive_key_from_parent_seed',
|
||||
'calculate_checksum',
|
||||
'verify_checksum',
|
||||
'lock_file',
|
||||
'prompt_for_password'
|
||||
]
|
208
src/utils/checksum.py
Normal file
208
src/utils/checksum.py
Normal file
@@ -0,0 +1,208 @@
|
||||
# utils/checksum.py
|
||||
|
||||
"""
|
||||
Checksum Module
|
||||
|
||||
This module provides functionalities to calculate and verify SHA-256 checksums for files.
|
||||
It ensures the integrity and authenticity of critical files within the application by
|
||||
comparing computed checksums against stored values.
|
||||
|
||||
Dependencies:
|
||||
- hashlib
|
||||
- logging
|
||||
- colored (from termcolor)
|
||||
- constants.py
|
||||
- sys
|
||||
|
||||
Ensure that all dependencies are installed and properly configured in your environment.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
import traceback
|
||||
from typing import Optional
|
||||
|
||||
from termcolor import colored
|
||||
|
||||
from constants import (
|
||||
APP_DIR,
|
||||
DATA_CHECKSUM_FILE,
|
||||
SCRIPT_CHECKSUM_FILE
|
||||
)
|
||||
|
||||
# Configure logging at the start of the module
|
||||
def configure_logging():
|
||||
"""
|
||||
Configures logging with both file and console handlers.
|
||||
Only ERROR and higher-level messages are shown in the terminal, while all messages
|
||||
are logged in the log file.
|
||||
"""
|
||||
# Create the 'logs' folder if it doesn't exist
|
||||
if not os.path.exists('logs'):
|
||||
os.makedirs('logs')
|
||||
|
||||
# Create a custom logger
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.DEBUG) # Set to DEBUG for detailed output
|
||||
|
||||
# Create handlers
|
||||
c_handler = logging.StreamHandler()
|
||||
f_handler = logging.FileHandler(os.path.join('logs', 'checksum.log')) # Log files will be in 'logs' folder
|
||||
|
||||
# Set levels: only errors and critical messages will be shown in the console
|
||||
c_handler.setLevel(logging.ERROR) # Terminal will show ERROR and above
|
||||
f_handler.setLevel(logging.DEBUG) # File will log everything from DEBUG and above
|
||||
|
||||
# Create formatters and add them to handlers, include file and line number in log messages
|
||||
c_format = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]')
|
||||
f_format = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]')
|
||||
|
||||
c_handler.setFormatter(c_format)
|
||||
f_handler.setFormatter(f_format)
|
||||
|
||||
# Add handlers to the logger
|
||||
logger.addHandler(c_handler)
|
||||
logger.addHandler(f_handler)
|
||||
|
||||
# Call the logging configuration function
|
||||
configure_logging()
|
||||
|
||||
def calculate_checksum(file_path: str) -> Optional[str]:
|
||||
"""
|
||||
Calculates the SHA-256 checksum of the given file.
|
||||
|
||||
Parameters:
|
||||
file_path (str): Path to the file.
|
||||
|
||||
Returns:
|
||||
Optional[str]: Hexadecimal SHA-256 checksum if successful, None otherwise.
|
||||
"""
|
||||
hasher = hashlib.sha256()
|
||||
try:
|
||||
with open(file_path, 'rb') as f:
|
||||
for chunk in iter(lambda: f.read(4096), b""):
|
||||
hasher.update(chunk)
|
||||
checksum = hasher.hexdigest()
|
||||
logging.debug(f"Calculated checksum for '{file_path}': {checksum}")
|
||||
return checksum
|
||||
except FileNotFoundError:
|
||||
logging.error(f"File '{file_path}' not found for checksum calculation.")
|
||||
print(colored(f"Error: File '{file_path}' not found for checksum calculation.", 'red'))
|
||||
return None
|
||||
except Exception as e:
|
||||
logging.error(f"Error calculating checksum for '{file_path}': {e}")
|
||||
logging.error(traceback.format_exc()) # Log full traceback
|
||||
print(colored(f"Error: Failed to calculate checksum for '{file_path}': {e}", 'red'))
|
||||
return None
|
||||
|
||||
|
||||
def verify_checksum(current_checksum: str, checksum_file_path: str) -> bool:
|
||||
"""
|
||||
Verifies the current checksum against the stored checksum.
|
||||
|
||||
Parameters:
|
||||
current_checksum (str): The newly calculated checksum.
|
||||
checksum_file_path (str): The checksum file to verify against.
|
||||
|
||||
Returns:
|
||||
bool: True if checksums match, False otherwise.
|
||||
"""
|
||||
try:
|
||||
with open(checksum_file_path, 'r') as f:
|
||||
stored_checksum = f.read().strip()
|
||||
if current_checksum == stored_checksum:
|
||||
logging.debug(f"Checksum verification passed for '{checksum_file_path}'.")
|
||||
return True
|
||||
else:
|
||||
logging.warning(f"Checksum mismatch for '{checksum_file_path}'.")
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
logging.error(f"Checksum file '{checksum_file_path}' not found.")
|
||||
print(colored(f"Error: Checksum file '{checksum_file_path}' not found.", 'red'))
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.error(f"Error reading checksum file '{checksum_file_path}': {e}")
|
||||
logging.error(traceback.format_exc()) # Log full traceback
|
||||
print(colored(f"Error: Failed to read checksum file '{checksum_file_path}': {e}", 'red'))
|
||||
return False
|
||||
|
||||
|
||||
def update_checksum(content: str, checksum_file_path: str) -> bool:
|
||||
"""
|
||||
Updates the stored checksum file with the provided content's checksum.
|
||||
|
||||
Parameters:
|
||||
content (str): The content to calculate the checksum for.
|
||||
checksum_file_path (str): The path to the checksum file to update.
|
||||
|
||||
Returns:
|
||||
bool: True if the checksum was successfully updated, False otherwise.
|
||||
"""
|
||||
try:
|
||||
hasher = hashlib.sha256()
|
||||
hasher.update(content.encode('utf-8'))
|
||||
new_checksum = hasher.hexdigest()
|
||||
with open(checksum_file_path, 'w') as f:
|
||||
f.write(new_checksum)
|
||||
logging.debug(f"Updated checksum for '{checksum_file_path}' to: {new_checksum}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to update checksum for '{checksum_file_path}': {e}")
|
||||
logging.error(traceback.format_exc()) # Log full traceback
|
||||
print(colored(f"Error: Failed to update checksum for '{checksum_file_path}': {e}", 'red'))
|
||||
return False
|
||||
|
||||
|
||||
def verify_and_update_checksum(file_path: str, checksum_file_path: str) -> bool:
|
||||
"""
|
||||
Verifies the checksum of a file against its stored checksum and updates it if necessary.
|
||||
|
||||
Parameters:
|
||||
file_path (str): Path to the file to verify.
|
||||
checksum_file_path (str): Path to the checksum file.
|
||||
|
||||
Returns:
|
||||
bool: True if verification is successful, False otherwise.
|
||||
"""
|
||||
current_checksum = calculate_checksum(file_path)
|
||||
if current_checksum is None:
|
||||
return False
|
||||
|
||||
if verify_checksum(current_checksum, checksum_file_path):
|
||||
print(colored(f"Checksum verification passed for '{file_path}'.", 'green'))
|
||||
logging.info(f"Checksum verification passed for '{file_path}'.")
|
||||
return True
|
||||
else:
|
||||
print(colored(f"Checksum verification failed for '{file_path}'.", 'red'))
|
||||
logging.warning(f"Checksum verification failed for '{file_path}'.")
|
||||
return False
|
||||
|
||||
|
||||
def initialize_checksum(file_path: str, checksum_file_path: str) -> bool:
|
||||
"""
|
||||
Initializes the checksum file by calculating the checksum of the given file.
|
||||
|
||||
Parameters:
|
||||
file_path (str): Path to the file to calculate checksum for.
|
||||
checksum_file_path (str): Path to the checksum file to create.
|
||||
|
||||
Returns:
|
||||
bool: True if initialization is successful, False otherwise.
|
||||
"""
|
||||
checksum = calculate_checksum(file_path)
|
||||
if checksum is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(checksum_file_path, 'w') as f:
|
||||
f.write(checksum)
|
||||
logging.debug(f"Initialized checksum file '{checksum_file_path}' with checksum: {checksum}")
|
||||
print(colored(f"Initialized checksum for '{file_path}'.", 'green'))
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to initialize checksum file '{checksum_file_path}': {e}")
|
||||
logging.error(traceback.format_exc()) # Log full traceback
|
||||
print(colored(f"Error: Failed to initialize checksum file '{checksum_file_path}': {e}", 'red'))
|
||||
return False
|
167
src/utils/file_lock.py
Normal file
167
src/utils/file_lock.py
Normal file
@@ -0,0 +1,167 @@
|
||||
# utils/file_lock.py
|
||||
|
||||
"""
|
||||
File Lock Module
|
||||
|
||||
This module provides a single context manager, `lock_file`, for acquiring and releasing
|
||||
locks on files using the `fcntl` library. It ensures that critical files are accessed
|
||||
safely, preventing race conditions and maintaining data integrity when multiple processes
|
||||
or threads attempt to read from or write to the same file concurrently.
|
||||
|
||||
Dependencies:
|
||||
- fcntl
|
||||
- logging
|
||||
- contextlib
|
||||
- typing
|
||||
- pathlib.Path
|
||||
- termcolor (for colored terminal messages)
|
||||
- sys
|
||||
|
||||
Ensure that all dependencies are installed and properly configured in your environment.
|
||||
"""
|
||||
|
||||
import os
|
||||
import fcntl
|
||||
import logging
|
||||
from contextlib import contextmanager
|
||||
from typing import Generator
|
||||
from pathlib import Path
|
||||
from termcolor import colored
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
# Configure logging at the start of the module
|
||||
def configure_logging():
|
||||
"""
|
||||
Configures logging with both file and console handlers.
|
||||
Only ERROR and higher-level messages are shown in the terminal, while all messages
|
||||
are logged in the log file.
|
||||
"""
|
||||
# Create a custom logger
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.DEBUG) # Set to DEBUG for detailed output
|
||||
|
||||
# Create the 'logs' folder if it doesn't exist
|
||||
if not os.path.exists('logs'):
|
||||
os.makedirs('logs')
|
||||
|
||||
# Create handlers
|
||||
c_handler = logging.StreamHandler()
|
||||
f_handler = logging.FileHandler(os.path.join('logs', 'file_lock.log')) # Log file in 'logs' folder
|
||||
|
||||
# Set levels: only errors and critical messages will be shown in the console
|
||||
c_handler.setLevel(logging.ERROR) # Console will show ERROR and above
|
||||
f_handler.setLevel(logging.DEBUG) # File will log everything from DEBUG and above
|
||||
|
||||
# Create formatters and add them to handlers, include file and line number in log messages
|
||||
c_format = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]')
|
||||
f_format = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]')
|
||||
|
||||
c_handler.setFormatter(c_format)
|
||||
f_handler.setFormatter(f_format)
|
||||
|
||||
# Add handlers to the logger
|
||||
if not logger.handlers:
|
||||
logger.addHandler(c_handler)
|
||||
logger.addHandler(f_handler)
|
||||
|
||||
# Call the logging configuration function
|
||||
configure_logging()
|
||||
|
||||
@contextmanager
|
||||
def lock_file(file_path: Path, lock_type: int) -> Generator[None, None, None]:
|
||||
"""
|
||||
Context manager to acquire a lock on a file.
|
||||
|
||||
Parameters:
|
||||
file_path (Path): The path to the file to lock.
|
||||
lock_type (int): The type of lock to acquire (`fcntl.LOCK_EX` for exclusive,
|
||||
`fcntl.LOCK_SH` for shared).
|
||||
|
||||
Yields:
|
||||
None
|
||||
|
||||
Raises:
|
||||
ValueError: If an invalid lock type is provided.
|
||||
SystemExit: Exits the program if the lock cannot be acquired.
|
||||
"""
|
||||
if lock_type not in (fcntl.LOCK_EX, fcntl.LOCK_SH):
|
||||
logging.error(f"Invalid lock type: {lock_type}. Use fcntl.LOCK_EX or fcntl.LOCK_SH.")
|
||||
print(colored("Error: Invalid lock type provided.", 'red'))
|
||||
sys.exit(1)
|
||||
|
||||
file = None
|
||||
try:
|
||||
# Determine the mode based on whether the file exists
|
||||
mode = 'rb+' if file_path.exists() else 'wb'
|
||||
|
||||
# Open the file
|
||||
file = open(file_path, mode)
|
||||
logging.debug(f"Opened file '{file_path}' in mode '{mode}' for locking.")
|
||||
|
||||
# Acquire the lock
|
||||
fcntl.flock(file, lock_type)
|
||||
lock_type_str = "Exclusive" if lock_type == fcntl.LOCK_EX else "Shared"
|
||||
logging.debug(f"{lock_type_str} lock acquired on '{file_path}'.")
|
||||
yield # Control is transferred to the block inside the `with` statement
|
||||
|
||||
except IOError as e:
|
||||
lock_type_str = "exclusive" if lock_type == fcntl.LOCK_EX else "shared"
|
||||
logging.error(f"Failed to acquire {lock_type_str} lock on '{file_path}': {e}")
|
||||
logging.error(traceback.format_exc()) # Log full traceback
|
||||
print(colored(f"Error: Failed to acquire {lock_type_str} lock on '{file_path}': {e}", 'red'))
|
||||
sys.exit(1)
|
||||
|
||||
finally:
|
||||
if file:
|
||||
try:
|
||||
# Release the lock
|
||||
fcntl.flock(file, fcntl.LOCK_UN)
|
||||
logging.debug(f"Lock released on '{file_path}'.")
|
||||
except Exception as e:
|
||||
lock_type_str = "exclusive" if lock_type == fcntl.LOCK_EX else "shared"
|
||||
logging.warning(f"Failed to release {lock_type_str} lock on '{file_path}': {e}")
|
||||
logging.error(traceback.format_exc()) # Log full traceback
|
||||
print(colored(f"Warning: Failed to release {lock_type_str} lock on '{file_path}': {e}", 'yellow'))
|
||||
finally:
|
||||
# Close the file
|
||||
try:
|
||||
file.close()
|
||||
logging.debug(f"File '{file_path}' closed successfully.")
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to close file '{file_path}': {e}")
|
||||
logging.error(traceback.format_exc()) # Log full traceback
|
||||
print(colored(f"Warning: Failed to close file '{file_path}': {e}", 'yellow'))
|
||||
|
||||
|
||||
@contextmanager
|
||||
def exclusive_lock(file_path: Path) -> Generator[None, None, None]:
|
||||
"""
|
||||
Convenience context manager to acquire an exclusive lock on a file.
|
||||
|
||||
Parameters:
|
||||
file_path (Path): The path to the file to lock.
|
||||
|
||||
Yields:
|
||||
None
|
||||
"""
|
||||
with lock_file(file_path, fcntl.LOCK_EX):
|
||||
yield
|
||||
|
||||
|
||||
@contextmanager
|
||||
def shared_lock(file_path: Path) -> Generator[None, None, None]:
|
||||
"""
|
||||
Convenience context manager to acquire a shared lock on a file.
|
||||
|
||||
Parameters:
|
||||
file_path (Path): The path to the file to lock.
|
||||
|
||||
Yields:
|
||||
None
|
||||
"""
|
||||
with lock_file(file_path, fcntl.LOCK_SH):
|
||||
yield
|
179
src/utils/key_derivation.py
Normal file
179
src/utils/key_derivation.py
Normal file
@@ -0,0 +1,179 @@
|
||||
# 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 softwares 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.
|
||||
|
||||
Dependencies:
|
||||
- hashlib
|
||||
- base64
|
||||
- unicodedata
|
||||
- logging
|
||||
|
||||
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
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
# Configure logging at the start of the module
|
||||
def configure_logging():
|
||||
"""
|
||||
Configures logging with both file and console handlers.
|
||||
Only ERROR and higher-level messages are shown in the terminal, while all messages
|
||||
are logged in the log file.
|
||||
"""
|
||||
# Create a custom logger
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG) # Set to DEBUG for detailed output
|
||||
|
||||
# Create the 'logs' folder if it doesn't exist
|
||||
if not os.path.exists('logs'):
|
||||
os.makedirs('logs')
|
||||
|
||||
# Create handlers
|
||||
c_handler = logging.StreamHandler()
|
||||
f_handler = logging.FileHandler(os.path.join('logs', 'key_derivation.log')) # Log file in 'logs' folder
|
||||
|
||||
# Set levels: only errors and critical messages will be shown in the console
|
||||
c_handler.setLevel(logging.ERROR) # Console will show ERROR and above
|
||||
f_handler.setLevel(logging.DEBUG) # File will log everything from DEBUG and above
|
||||
|
||||
# Create formatters and add them to handlers, include file and line number in log messages
|
||||
c_format = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]')
|
||||
f_format = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]')
|
||||
|
||||
c_handler.setFormatter(c_format)
|
||||
f_handler.setFormatter(f_format)
|
||||
|
||||
# Add handlers to the logger
|
||||
if not logger.handlers:
|
||||
logger.addHandler(c_handler)
|
||||
logger.addHandler(f_handler)
|
||||
|
||||
# Call the logging configuration function
|
||||
configure_logging()
|
||||
|
||||
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, iterations: int = 100_000) -> bytes:
|
||||
"""
|
||||
Derives a Fernet-compatible encryption key from a BIP-39 parent seed using PBKDF2-HMAC-SHA256.
|
||||
|
||||
This function normalizes the parent seed 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:
|
||||
parent_seed (str): The 12-word BIP-39 parent seed phrase.
|
||||
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 parent seed is empty or does not meet the word count requirements.
|
||||
"""
|
||||
if not parent_seed:
|
||||
logger.error("Parent seed cannot be empty.")
|
||||
raise ValueError("Parent seed cannot be empty.")
|
||||
|
||||
word_count = len(parent_seed.strip().split())
|
||||
if word_count != 12:
|
||||
logger.error(f"Parent seed must be exactly 12 words, but {word_count} were provided.")
|
||||
raise ValueError(f"Parent seed must be exactly 12 words, but {word_count} were provided.")
|
||||
|
||||
# Normalize the parent seed to NFKD form and encode to UTF-8
|
||||
normalized_seed = unicodedata.normalize('NFKD', parent_seed).strip()
|
||||
seed_bytes = normalized_seed.encode('utf-8')
|
||||
|
||||
try:
|
||||
# Derive the key using PBKDF2-HMAC-SHA256
|
||||
logger.debug("Starting key derivation from parent seed.")
|
||||
key = hashlib.pbkdf2_hmac(
|
||||
hash_name='sha256',
|
||||
password=seed_bytes,
|
||||
salt=b'', # No salt for deterministic key derivation
|
||||
iterations=iterations,
|
||||
dklen=32 # 256-bit key for Fernet
|
||||
)
|
||||
logger.debug(f"Derived key from parent seed (hex): {key.hex()}")
|
||||
|
||||
# Encode the key in URL-safe base64
|
||||
key_b64 = base64.urlsafe_b64encode(key)
|
||||
logger.debug(f"Base64-encoded key from parent seed: {key_b64.decode()}")
|
||||
|
||||
return key_b64
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error deriving key from parent seed: {e}")
|
||||
logger.error(traceback.format_exc()) # Log full traceback
|
||||
raise
|
218
src/utils/password_prompt.py
Normal file
218
src/utils/password_prompt.py
Normal file
@@ -0,0 +1,218 @@
|
||||
# utils/password_prompt.py
|
||||
|
||||
"""
|
||||
Password Prompt Module
|
||||
|
||||
This module provides functions to securely prompt users for passwords, ensuring that passwords
|
||||
are entered and confirmed correctly. It handles both the creation of new passwords and the
|
||||
input of existing passwords for decryption purposes. By centralizing password prompting logic,
|
||||
this module enhances code reuse, security, and maintainability across the application.
|
||||
|
||||
Dependencies:
|
||||
- getpass
|
||||
- logging
|
||||
- colorama
|
||||
- termcolor
|
||||
- constants (for MIN_PASSWORD_LENGTH)
|
||||
|
||||
Ensure that all dependencies are installed and properly configured in your environment.
|
||||
"""
|
||||
|
||||
import os
|
||||
import getpass
|
||||
import logging
|
||||
import sys
|
||||
import unicodedata
|
||||
import traceback
|
||||
|
||||
from termcolor import colored
|
||||
from colorama import init as colorama_init
|
||||
|
||||
from constants import MIN_PASSWORD_LENGTH
|
||||
|
||||
# Initialize colorama for colored terminal text
|
||||
colorama_init()
|
||||
|
||||
# Configure logging at the start of the module
|
||||
def configure_logging():
|
||||
"""
|
||||
Configures logging with both file and console handlers.
|
||||
Only ERROR and higher-level messages are shown in the terminal, while all messages
|
||||
are logged in the log file.
|
||||
"""
|
||||
# Create a custom logger
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG) # Set to DEBUG for detailed output
|
||||
|
||||
# Create the 'logs' folder if it doesn't exist
|
||||
if not os.path.exists('logs'):
|
||||
os.makedirs('logs')
|
||||
|
||||
# Create handlers
|
||||
c_handler = logging.StreamHandler()
|
||||
f_handler = logging.FileHandler(os.path.join('logs', 'password_prompt.log')) # Log file in 'logs' folder
|
||||
|
||||
# Set levels: only errors and critical messages will be shown in the console
|
||||
c_handler.setLevel(logging.ERROR) # Console will show ERROR and above
|
||||
f_handler.setLevel(logging.DEBUG) # File will log everything from DEBUG and above
|
||||
|
||||
# Create formatters and add them to handlers, include file and line number in log messages
|
||||
c_format = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]')
|
||||
f_format = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]')
|
||||
|
||||
c_handler.setFormatter(c_format)
|
||||
f_handler.setFormatter(f_format)
|
||||
|
||||
# Add handlers to the logger
|
||||
if not logger.handlers:
|
||||
logger.addHandler(c_handler)
|
||||
logger.addHandler(f_handler)
|
||||
|
||||
# Call the logging configuration function
|
||||
configure_logging()
|
||||
|
||||
def prompt_new_password() -> str:
|
||||
"""
|
||||
Prompts the user to enter and confirm a new password for encrypting the parent seed.
|
||||
|
||||
This function ensures that the password meets the minimum length requirement and that the
|
||||
password and confirmation match. It provides user-friendly messages and handles retries.
|
||||
|
||||
Returns:
|
||||
str: The confirmed password entered by the user.
|
||||
|
||||
Raises:
|
||||
SystemExit: If the user fails to provide a valid password after multiple attempts.
|
||||
"""
|
||||
max_retries = 5
|
||||
attempts = 0
|
||||
|
||||
while attempts < max_retries:
|
||||
try:
|
||||
password = getpass.getpass(prompt="Enter a new password: ").strip()
|
||||
confirm_password = getpass.getpass(prompt="Confirm your password: ").strip()
|
||||
|
||||
if not password:
|
||||
print(colored("Error: Password cannot be empty. Please try again.", 'red'))
|
||||
logging.warning("User attempted to enter an empty password.")
|
||||
attempts += 1
|
||||
continue
|
||||
|
||||
if len(password) < MIN_PASSWORD_LENGTH:
|
||||
print(colored(f"Error: Password must be at least {MIN_PASSWORD_LENGTH} characters long.", 'red'))
|
||||
logging.warning(f"User entered a password shorter than {MIN_PASSWORD_LENGTH} characters.")
|
||||
attempts += 1
|
||||
continue
|
||||
|
||||
if password != confirm_password:
|
||||
print(colored("Error: Passwords do not match. Please try again.", 'red'))
|
||||
logging.warning("User entered mismatching passwords.")
|
||||
attempts += 1
|
||||
continue
|
||||
|
||||
# Normalize the password to NFKD form
|
||||
normalized_password = unicodedata.normalize('NFKD', password)
|
||||
logging.debug("User entered a valid and confirmed password.")
|
||||
return normalized_password
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print(colored("\nOperation cancelled by user.", 'yellow'))
|
||||
logging.info("Password prompt interrupted by user.")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
logging.error(f"Unexpected error during password prompt: {e}")
|
||||
logging.error(traceback.format_exc()) # Log full traceback
|
||||
print(colored(f"Error: {e}", 'red'))
|
||||
attempts += 1
|
||||
|
||||
print(colored("Maximum password attempts exceeded. Exiting.", 'red'))
|
||||
logging.error("User failed to provide a valid password after multiple attempts.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def prompt_existing_password(prompt_message: str = "Enter your password: ") -> str:
|
||||
"""
|
||||
Prompts the user to enter an existing password, typically used for decryption purposes.
|
||||
|
||||
This function ensures that the password is entered securely without echoing it to the terminal.
|
||||
|
||||
Parameters:
|
||||
prompt_message (str): The message displayed to prompt the user. Defaults to "Enter your password: ".
|
||||
|
||||
Returns:
|
||||
str: The password entered by the user.
|
||||
|
||||
Raises:
|
||||
SystemExit: If the user interrupts the operation.
|
||||
"""
|
||||
try:
|
||||
password = getpass.getpass(prompt=prompt_message).strip()
|
||||
|
||||
if not password:
|
||||
print(colored("Error: Password cannot be empty.", 'red'))
|
||||
logging.warning("User attempted to enter an empty password.")
|
||||
sys.exit(1)
|
||||
|
||||
# Normalize the password to NFKD form
|
||||
normalized_password = unicodedata.normalize('NFKD', password)
|
||||
logging.debug("User entered an existing password for decryption.")
|
||||
return normalized_password
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print(colored("\nOperation cancelled by user.", 'yellow'))
|
||||
logging.info("Existing password prompt interrupted by user.")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
logging.error(f"Unexpected error during existing password prompt: {e}")
|
||||
logging.error(traceback.format_exc()) # Log full traceback
|
||||
print(colored(f"Error: {e}", 'red'))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def confirm_action(prompt_message: str = "Are you sure you want to proceed? (Y/N): ") -> bool:
|
||||
"""
|
||||
Prompts the user to confirm an action, typically used before performing critical operations.
|
||||
|
||||
Parameters:
|
||||
prompt_message (str): The confirmation message displayed to the user. Defaults to
|
||||
"Are you sure you want to proceed? (Y/N): ".
|
||||
|
||||
Returns:
|
||||
bool: True if the user confirms the action, False otherwise.
|
||||
|
||||
Raises:
|
||||
SystemExit: If the user interrupts the operation.
|
||||
"""
|
||||
try:
|
||||
while True:
|
||||
response = input(colored(prompt_message, 'cyan')).strip().lower()
|
||||
if response in ['y', 'yes']:
|
||||
logging.debug("User confirmed the action.")
|
||||
return True
|
||||
elif response in ['n', 'no']:
|
||||
logging.debug("User declined the action.")
|
||||
return False
|
||||
else:
|
||||
print(colored("Please respond with 'Y' or 'N'.", 'yellow'))
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print(colored("\nOperation cancelled by user.", 'yellow'))
|
||||
logging.info("Action confirmation interrupted by user.")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
logging.error(f"Unexpected error during action confirmation: {e}")
|
||||
logging.error(traceback.format_exc()) # Log full traceback
|
||||
print(colored(f"Error: {e}", 'red'))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def prompt_for_password() -> str:
|
||||
"""
|
||||
Prompts the user to enter a new password by invoking the prompt_new_password function.
|
||||
|
||||
This function serves as an alias to maintain consistency with import statements in other modules.
|
||||
|
||||
Returns:
|
||||
str: The confirmed password entered by the user.
|
||||
"""
|
||||
return prompt_new_password()
|
Reference in New Issue
Block a user