This commit is contained in:
thePR0M3TH3AN
2024-10-26 22:56:57 -04:00
parent 6090c38b92
commit 7d4eef2110
22 changed files with 1759 additions and 1152 deletions

View File

@@ -11,192 +11,125 @@ corrupted or lost data by maintaining timestamped backups.
Ensure that all dependencies are installed and properly configured in your environment.
"""
import logging
import os
import shutil
import time
import logging
import traceback
from pathlib import Path
from colorama import Fore
from termcolor import colored
from constants import APP_DIR, INDEX_FILE
from utils.file_lock import lock_file
from constants import APP_DIR
# 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', 'backup_manager.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()
# Instantiate the logger
logger = logging.getLogger(__name__)
class BackupManager:
"""
BackupManager Class
Handles the creation, restoration, and listing of backups for the encrypted
password index file. Backups are stored in the application directory with
Handles the creation, restoration, and listing of backups for the encrypted password
index file. Backups are stored in the application directory with
timestamped filenames to facilitate easy identification and retrieval.
"""
BACKUP_FILENAME_TEMPLATE = 'passwords_db_backup_{timestamp}.json.enc'
def __init__(self):
def __init__(self, fingerprint_dir: Path):
"""
Initializes the BackupManager with the application directory and index file paths.
Initializes the BackupManager with the fingerprint directory.
Parameters:
fingerprint_dir (Path): The directory corresponding to the fingerprint.
"""
self.app_dir = APP_DIR
self.index_file = INDEX_FILE
logging.debug(f"BackupManager initialized with APP_DIR: {self.app_dir} and INDEX_FILE: {self.index_file}")
self.fingerprint_dir = fingerprint_dir
self.backup_dir = self.fingerprint_dir / 'backups'
self.backup_dir.mkdir(parents=True, exist_ok=True)
self.index_file = self.fingerprint_dir / 'seedpass_passwords_db.json.enc'
logger.debug(f"BackupManager initialized with backup directory at {self.backup_dir}")
def create_backup(self) -> None:
"""
Creates a timestamped backup of the encrypted password index file.
The backup file is named using the current Unix timestamp to ensure uniqueness.
If the index file does not exist, no backup is created.
Raises:
Exception: If the backup process fails due to I/O errors.
"""
if not self.index_file.exists():
logging.warning("Index file does not exist. No backup created.")
print(colored("Warning: Index file does not exist. No backup created.", 'yellow'))
return
timestamp = int(time.time())
backup_filename = self.BACKUP_FILENAME_TEMPLATE.format(timestamp=timestamp)
backup_file = self.app_dir / backup_filename
try:
with lock_file(self.index_file, lock_type=fcntl.LOCK_SH):
shutil.copy2(self.index_file, backup_file)
logging.info(f"Backup created successfully at '{backup_file}'.")
index_file = self.index_file
if not index_file.exists():
logger.warning("Index file does not exist. No backup created.")
print(colored("Warning: Index file does not exist. No backup created.", 'yellow'))
return
timestamp = int(time.time())
backup_filename = self.BACKUP_FILENAME_TEMPLATE.format(timestamp=timestamp)
backup_file = self.backup_dir / backup_filename
shutil.copy2(index_file, backup_file)
logger.info(f"Backup created successfully at '{backup_file}'.")
print(colored(f"Backup created successfully at '{backup_file}'.", 'green'))
except Exception as e:
logging.error(f"Failed to create backup: {e}")
logging.error(traceback.format_exc()) # Log full traceback
logger.error(f"Failed to create backup: {e}")
logger.error(traceback.format_exc())
print(colored(f"Error: Failed to create backup: {e}", 'red'))
def restore_latest_backup(self) -> None:
"""
Restores the encrypted password index file from the latest available backup.
The latest backup is determined based on the Unix timestamp in the backup filenames.
If no backups are found, an error message is displayed.
Raises:
Exception: If the restoration process fails due to I/O errors or missing backups.
"""
backup_files = sorted(
self.app_dir.glob('passwords_db_backup_*.json.enc'),
key=lambda x: x.stat().st_mtime,
reverse=True
)
if not backup_files:
logging.error("No backup files found to restore.")
print(colored("Error: No backup files found to restore.", 'red'))
return
latest_backup = backup_files[0]
try:
with lock_file(latest_backup, lock_type=fcntl.LOCK_SH):
shutil.copy2(latest_backup, self.index_file)
logging.info(f"Restored the index file from backup '{latest_backup}'.")
backup_files = sorted(
self.backup_dir.glob('passwords_db_backup_*.json.enc'),
key=lambda x: x.stat().st_mtime,
reverse=True
)
if not backup_files:
logger.error("No backup files found to restore.")
print(colored("Error: No backup files found to restore.", 'red'))
return
latest_backup = backup_files[0]
index_file = self.index_file
shutil.copy2(latest_backup, index_file)
logger.info(f"Restored the index file from backup '{latest_backup}'.")
print(colored(f"Restored the index file from backup '{latest_backup}'.", 'green'))
except Exception as e:
logging.error(f"Failed to restore from backup '{latest_backup}': {e}")
logging.error(traceback.format_exc()) # Log full traceback
logger.error(f"Failed to restore from backup '{latest_backup}': {e}")
logger.error(traceback.format_exc())
print(colored(f"Error: Failed to restore from backup '{latest_backup}': {e}", 'red'))
def list_backups(self) -> None:
"""
Lists all available backups in the application directory, sorted by date.
try:
backup_files = sorted(
self.backup_dir.glob('passwords_db_backup_*.json.enc'),
key=lambda x: x.stat().st_mtime,
reverse=True
)
Displays the backups with their filenames and creation dates.
"""
backup_files = sorted(
self.app_dir.glob('passwords_db_backup_*.json.enc'),
key=lambda x: x.stat().st_mtime,
reverse=True
)
if not backup_files:
logger.info("No backup files available.")
print(colored("No backup files available.", 'yellow'))
return
if not backup_files:
logging.info("No backup files available.")
print(colored("No backup files available.", 'yellow'))
return
print(colored("Available Backups:", 'cyan'))
for backup in backup_files:
creation_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(backup.stat().st_mtime))
print(colored(f"- {backup.name} (Created on: {creation_time})", 'cyan'))
print(colored("Available Backups:", 'cyan'))
for backup in backup_files:
creation_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(backup.stat().st_mtime))
print(colored(f"- {backup.name} (Created on: {creation_time})", 'cyan'))
except Exception as e:
logger.error(f"Failed to list backups: {e}")
logger.error(traceback.format_exc())
print(colored(f"Error: Failed to list backups: {e}", 'red'))
def restore_backup_by_timestamp(self, timestamp: int) -> None:
"""
Restores the encrypted password index file from a backup with the specified timestamp.
Parameters:
timestamp (int): The Unix timestamp of the backup to restore.
Raises:
Exception: If the restoration process fails due to I/O errors or missing backups.
"""
backup_filename = self.BACKUP_FILENAME_TEMPLATE.format(timestamp=timestamp)
backup_file = self.app_dir / backup_filename
backup_file = self.backup_dir / backup_filename
if not backup_file.exists():
logging.error(f"No backup found with timestamp {timestamp}.")
logger.error(f"No backup found with timestamp {timestamp}.")
print(colored(f"Error: No backup found with timestamp {timestamp}.", 'red'))
return
try:
with lock_file(backup_file, lock_type=fcntl.LOCK_SH):
shutil.copy2(backup_file, self.index_file)
logging.info(f"Restored the index file from backup '{backup_file}'.")
logger.info(f"Restored the index file from backup '{backup_file}'.")
print(colored(f"Restored the index file from backup '{backup_file}'.", 'green'))
except Exception as e:
logging.error(f"Failed to restore from backup '{backup_file}': {e}")
logging.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to restore from backup '{backup_file}': {e}", 'red'))
# Example usage (to be integrated within the PasswordManager class or other modules):
# from password_manager.backup import BackupManager
# backup_manager = BackupManager()
# backup_manager.create_backup()
# backup_manager.restore_latest_backup()
# backup_manager.list_backups()
# backup_manager.restore_backup_by_timestamp(1700000000) # Example timestamp
logger.error(f"Failed to restore from backup '{backup_file}': {e}")
logger.error(traceback.format_exc())
print(colored(f"Error: Failed to restore from backup '{backup_file}': {e}", 'red'))

View File

@@ -10,66 +10,24 @@ of the password index.
Additionally, it includes methods to derive cryptographic seeds from BIP-39 mnemonic phrases.
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.
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 are not appropriate for this software's use case.
"""
import os
import json
import stat
import hashlib
import logging
import traceback
import json
import hashlib
import os
from pathlib import Path
from typing import Optional
from cryptography.fernet import Fernet, InvalidToken
from utils.file_lock import exclusive_lock, shared_lock
from colorama import Fore
from termcolor import colored
from mnemonic import Mnemonic # Library for BIP-39 seed phrase handling
import fcntl # Required for lock_type constants in file_lock
from constants import INDEX_FILE # Ensure INDEX_FILE is imported correctly
# Configure logging at the start of the module
def configure_logging():
"""
Configures logging with both file and console handlers.
Logs include the timestamp, log level, message, filename, and line number.
Only errors and critical logs are shown in the terminal, while all logs are saved to a file.
"""
# Create the 'logs' folder if it doesn't exist
if not os.path.exists('logs'):
os.makedirs('logs')
# Create a custom logger for this module
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG) # Set to DEBUG for detailed output
# Create handlers
c_handler = logging.StreamHandler()
f_handler = logging.FileHandler(os.path.join('logs', 'encryption_manager.log')) # Log file 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
formatter = logging.Formatter(
'%(asctime)s [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]'
)
c_handler.setFormatter(formatter)
f_handler.setFormatter(formatter)
# Add handlers to the logger if not already added
if not logger.handlers:
logger.addHandler(c_handler)
logger.addHandler(f_handler)
# Call the logging configuration function
configure_logging()
from utils.file_lock import lock_file # Ensure this utility is correctly implemented
import fcntl # For file locking
# Instantiate the logger
logger = logging.getLogger(__name__)
class EncryptionManager:
@@ -78,153 +36,217 @@ class EncryptionManager:
Manages the encryption and decryption of data and files using a Fernet encryption key.
"""
def __init__(self, encryption_key: bytes, fingerprint_dir: Path):
"""
Initializes the EncryptionManager with the provided encryption key and fingerprint directory.
Parameters:
encryption_key (bytes): The Fernet encryption key.
fingerprint_dir (Path): The directory corresponding to the fingerprint.
"""
self.fingerprint_dir = fingerprint_dir
self.parent_seed_file = self.fingerprint_dir / 'parent_seed.enc'
self.key = encryption_key
def __init__(self, encryption_key: bytes):
try:
self.fernet = Fernet(encryption_key)
logger.debug("EncryptionManager initialized with provided encryption key.")
self.fernet = Fernet(self.key)
logger.debug(f"EncryptionManager initialized for {self.fingerprint_dir}")
except Exception as e:
logger.error(f"Failed to initialize Fernet with provided encryption key: {e}")
logger.error(traceback.format_exc())
print(colored(f"Error: Failed to initialize encryption manager: {e}", 'red'))
raise
def encrypt_parent_seed(self, parent_seed, file_path: Path) -> None:
def encrypt_parent_seed(self, parent_seed: str) -> None:
"""
Encrypts and saves the parent seed to the specified file.
Encrypts and saves the parent seed to 'parent_seed.enc' within the fingerprint directory.
:param parent_seed: The BIP39 parent seed phrase or Bip39Mnemonic object.
:param file_path: The path to the file where the encrypted parent seed will be saved.
:param parent_seed: The BIP39 parent seed phrase.
"""
try:
# Convert Bip39Mnemonic to string if necessary
if hasattr(parent_seed, 'ToStr'):
parent_seed = parent_seed.ToStr()
# Now encode the string
# Convert seed to bytes
data = parent_seed.encode('utf-8')
# Encrypt and save the data
# Encrypt the data
encrypted_data = self.encrypt_data(data)
with open(file_path, 'wb') as f:
f.write(encrypted_data)
logging.info(f"Parent seed encrypted and saved to '{file_path}'.")
print(colored(f"Parent seed encrypted and saved to '{file_path}'.", 'green'))
# Write the encrypted data to the file with locking
with lock_file(self.parent_seed_file, fcntl.LOCK_EX):
with open(self.parent_seed_file, 'wb') as f:
f.write(encrypted_data)
# Set file permissions to read/write for the user only
os.chmod(self.parent_seed_file, 0o600)
logger.info(f"Parent seed encrypted and saved to '{self.parent_seed_file}'.")
print(colored(f"Parent seed encrypted and saved to '{self.parent_seed_file}'.", 'green'))
except Exception as e:
logging.error(f"Failed to encrypt and save parent seed: {e}")
logging.error(traceback.format_exc())
logger.error(f"Failed to encrypt and save parent seed: {e}")
logger.error(traceback.format_exc())
print(colored(f"Error: Failed to encrypt and save parent seed: {e}", 'red'))
raise
def encrypt_file(self, file_path: Path, data: bytes) -> None:
def decrypt_parent_seed(self) -> str:
"""
Encrypts the provided data and writes it to the specified file with file locking.
Decrypts and returns the parent seed from 'parent_seed.enc' within the fingerprint directory.
:param file_path: The path to the file where encrypted data will be written.
:param data: The plaintext data to encrypt and write.
:return: The decrypted parent seed.
"""
try:
encrypted_data = self.encrypt_data(data)
with exclusive_lock(file_path):
with open(file_path, 'wb') as file:
file.write(encrypted_data)
logger.debug(f"Encrypted data written to '{file_path}'.")
print(colored(f"Encrypted data written to '{file_path}'.", 'green'))
parent_seed_path = self.fingerprint_dir / 'parent_seed.enc'
with lock_file(parent_seed_path, fcntl.LOCK_SH):
with open(parent_seed_path, 'rb') as f:
encrypted_data = f.read()
decrypted_data = self.decrypt_data(encrypted_data)
parent_seed = decrypted_data.decode('utf-8').strip()
logger.debug(f"Parent seed decrypted successfully from '{parent_seed_path}'.")
return parent_seed
except InvalidToken:
logger.error("Invalid encryption key or corrupted data while decrypting parent seed.")
print(colored("Error: Invalid encryption key or corrupted data.", 'red'))
raise
except Exception as e:
logger.error(f"Failed to encrypt and write to file '{file_path}': {e}")
logger.error(f"Failed to decrypt parent seed: {e}")
logger.error(traceback.format_exc())
print(colored(f"Error: Failed to encrypt and write to file '{file_path}': {e}", 'red'))
print(colored(f"Error: Failed to decrypt parent seed: {e}", 'red'))
raise
def encrypt_data(self, data: bytes) -> bytes:
"""
Encrypts the given plaintext data.
Encrypts the given data using Fernet.
:param data: The plaintext data to encrypt.
:return: The encrypted data as bytes.
:param data: Data to encrypt.
:return: Encrypted data.
"""
try:
encrypted_data = self.fernet.encrypt(data)
logger.debug("Data encrypted successfully.")
return encrypted_data
except Exception as e:
logger.error(f"Error encrypting data: {e}")
logger.error(f"Failed to encrypt data: {e}")
logger.error(traceback.format_exc())
print(colored(f"Error: Failed to encrypt data: {e}", 'red'))
raise
def decrypt_data(self, encrypted_data: bytes) -> bytes:
"""
Decrypts the given encrypted data.
Decrypts the provided encrypted data using the derived key.
:param encrypted_data: The encrypted data to decrypt.
:return: The decrypted plaintext data as bytes.
:return: The decrypted data as bytes.
"""
try:
decrypted_data = self.fernet.decrypt(encrypted_data)
logger.debug("Data decrypted successfully.")
return decrypted_data
except InvalidToken:
logger.error("Invalid encryption key or corrupted data.")
logger.error("Invalid encryption key or corrupted data while decrypting data.")
print(colored("Error: Invalid encryption key or corrupted data.", 'red'))
raise
except Exception as e:
logger.error(f"Error decrypting data: {e}")
logger.error(traceback.format_exc()) # Log full traceback
logger.error(f"Failed to decrypt data: {e}")
logger.error(traceback.format_exc())
print(colored(f"Error: Failed to decrypt data: {e}", 'red'))
raise
def decrypt_file(self, file_path: Path) -> bytes:
def encrypt_and_save_file(self, data: bytes, relative_path: Path) -> None:
"""
Decrypts the data from the specified file.
Encrypts data and saves it to a specified relative path within the fingerprint directory.
:param file_path: The path to the file containing encrypted data.
:return: The decrypted plaintext data as bytes.
:param data: Data to encrypt.
:param relative_path: Relative path within the fingerprint directory to save the encrypted data.
"""
try:
with shared_lock(file_path):
with open(file_path, 'rb') as file:
encrypted_data = file.read()
decrypted_data = self.decrypt_data(encrypted_data)
logger.debug(f"Decrypted data read from '{file_path}'.")
print(colored(f"Decrypted data read from '{file_path}'.", 'green'))
return decrypted_data
# Define the full path
file_path = self.fingerprint_dir / relative_path
# Ensure the parent directories exist
file_path.parent.mkdir(parents=True, exist_ok=True)
# Encrypt the data
encrypted_data = self.encrypt_data(data)
# Write the encrypted data to the file with locking
with lock_file(file_path, fcntl.LOCK_EX):
with open(file_path, 'wb') as f:
f.write(encrypted_data)
# Set file permissions to read/write for the user only
os.chmod(file_path, 0o600)
logger.info(f"Data encrypted and saved to '{file_path}'.")
print(colored(f"Data encrypted and saved to '{file_path}'.", 'green'))
except Exception as e:
logger.error(f"Failed to decrypt file '{file_path}': {e}")
logger.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to decrypt file '{file_path}': {e}", 'red'))
logger.error(f"Failed to encrypt and save data to '{relative_path}': {e}")
logger.error(traceback.format_exc())
print(colored(f"Error: Failed to encrypt and save data to '{relative_path}': {e}", 'red'))
raise
def save_json_data(self, data: dict, file_path: Optional[Path] = None) -> None:
def decrypt_file(self, relative_path: Path) -> bytes:
"""
Encrypts and saves the provided JSON data to the specified file.
Decrypts data from a specified relative path within the fingerprint directory.
:param relative_path: Relative path within the fingerprint directory to decrypt the data from.
:return: Decrypted data as bytes.
"""
try:
# Define the full path
file_path = self.fingerprint_dir / relative_path
# Read the encrypted data with locking
with lock_file(file_path, fcntl.LOCK_SH):
with open(file_path, 'rb') as f:
encrypted_data = f.read()
# Decrypt the data
decrypted_data = self.decrypt_data(encrypted_data)
logger.debug(f"Data decrypted successfully from '{file_path}'.")
return decrypted_data
except InvalidToken:
logger.error("Invalid encryption key or corrupted data while decrypting file.")
print(colored("Error: Invalid encryption key or corrupted data.", 'red'))
raise
except Exception as e:
logger.error(f"Failed to decrypt data from '{relative_path}': {e}")
logger.error(traceback.format_exc())
print(colored(f"Error: Failed to decrypt data from '{relative_path}': {e}", 'red'))
raise
def save_json_data(self, data: dict, relative_path: Optional[Path] = None) -> None:
"""
Encrypts and saves the provided JSON data to the specified relative path within the fingerprint directory.
:param data: The JSON data to save.
:param file_path: The path to the file where data will be saved. Defaults to INDEX_FILE.
:param relative_path: The relative path within the fingerprint directory where data will be saved.
Defaults to 'seedpass_passwords_db.json.enc'.
"""
if file_path is None:
file_path = INDEX_FILE
if relative_path is None:
relative_path = Path('seedpass_passwords_db.json.enc')
try:
json_data = json.dumps(data, indent=4).encode('utf-8')
self.encrypt_file(file_path, json_data)
logger.debug(f"JSON data encrypted and saved to '{file_path}'.")
print(colored(f"JSON data encrypted and saved to '{file_path}'.", 'green'))
self.encrypt_and_save_file(json_data, relative_path)
logger.debug(f"JSON data encrypted and saved to '{relative_path}'.")
print(colored(f"JSON data encrypted and saved to '{relative_path}'.", 'green'))
except Exception as e:
logger.error(f"Failed to save JSON data to '{file_path}': {e}")
logger.error(f"Failed to save JSON data to '{relative_path}': {e}")
logger.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to save JSON data to '{file_path}': {e}", 'red'))
print(colored(f"Error: Failed to save JSON data to '{relative_path}': {e}", 'red'))
raise
def load_json_data(self, file_path: Optional[Path] = None) -> dict:
def load_json_data(self, relative_path: Optional[Path] = None) -> dict:
"""
Decrypts and loads JSON data from the specified file.
Decrypts and loads JSON data from the specified relative path within the fingerprint directory.
:param file_path: The path to the file from which data will be loaded. Defaults to INDEX_FILE.
:param relative_path: The relative path within the fingerprint directory from which data will be loaded.
Defaults to 'seedpass_passwords_db.json.enc'.
:return: The decrypted JSON data as a dictionary.
"""
if file_path is None:
file_path = INDEX_FILE
if relative_path is None:
relative_path = Path('seedpass_passwords_db.json.enc')
file_path = self.fingerprint_dir / relative_path
if not file_path.exists():
logger.info(f"Index file '{file_path}' does not exist. Initializing empty data.")
@@ -232,7 +254,7 @@ class EncryptionManager:
return {'passwords': {}}
try:
decrypted_data = self.decrypt_file(file_path)
decrypted_data = self.decrypt_file(relative_path)
json_content = decrypted_data.decode('utf-8').strip()
data = json.loads(json_content)
logger.debug(f"JSON data loaded and decrypted from '{file_path}': {data}")
@@ -244,7 +266,7 @@ class EncryptionManager:
print(colored(f"Error: Failed to decode JSON data from '{file_path}': {e}", 'red'))
raise
except InvalidToken:
logger.error("Invalid encryption key or corrupted data.")
logger.error("Invalid encryption key or corrupted data while decrypting JSON data.")
print(colored("Error: Invalid encryption key or corrupted data.", 'red'))
raise
except Exception as e:
@@ -253,28 +275,40 @@ class EncryptionManager:
print(colored(f"Error: Failed to load JSON data from '{file_path}': {e}", 'red'))
raise
def update_checksum(self, file_path: Optional[Path] = None) -> None:
def update_checksum(self, relative_path: Optional[Path] = None) -> None:
"""
Updates the checksum file for the specified file.
Updates the checksum file for the specified file within the fingerprint directory.
:param file_path: The path to the file for which the checksum will be updated.
Defaults to INDEX_FILE.
:param relative_path: The relative path within the fingerprint directory for which the checksum will be updated.
Defaults to 'seedpass_passwords_db.json.enc'.
"""
if file_path is None:
file_path = INDEX_FILE
if relative_path is None:
relative_path = Path('seedpass_passwords_db.json.enc')
try:
decrypted_data = self.decrypt_file(file_path)
file_path = self.fingerprint_dir / relative_path
decrypted_data = self.decrypt_file(relative_path)
content = decrypted_data.decode('utf-8')
logger.debug("Calculating checksum of the updated file content.")
checksum = hashlib.sha256(content.encode('utf-8')).hexdigest()
logger.debug(f"New checksum: {checksum}")
checksum_file = file_path.parent / f"{file_path.stem}_checksum.txt"
with open(checksum_file, 'w') as f:
f.write(checksum)
# Write the checksum to the file with locking
with lock_file(checksum_file, fcntl.LOCK_EX):
with open(checksum_file, 'w') as f:
f.write(checksum)
# Set file permissions to read/write for the user only
os.chmod(checksum_file, 0o600)
logger.debug(f"Checksum for '{file_path}' updated and written to '{checksum_file}'.")
print(colored(f"Checksum for '{file_path}' updated.", 'green'))
except Exception as e:
logger.error(f"Failed to update checksum for '{file_path}': {e}")
logger.error(f"Failed to update checksum for '{relative_path}': {e}")
logger.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to update checksum for '{file_path}': {e}", 'red'))
print(colored(f"Error: Failed to update checksum for '{relative_path}': {e}", 'red'))
raise
def get_encrypted_index(self) -> Optional[bytes]:
@@ -283,56 +317,47 @@ class EncryptionManager:
:return: Encrypted data as bytes or None if the index file does not exist.
"""
if not INDEX_FILE.exists():
logger.error(f"Index file '{INDEX_FILE}' does not exist.")
print(colored(f"Error: Index file '{INDEX_FILE}' does not exist.", 'red'))
return None
try:
with shared_lock(INDEX_FILE):
with open(INDEX_FILE, 'rb') as file:
relative_path = Path('seedpass_passwords_db.json.enc')
if not (self.fingerprint_dir / relative_path).exists():
logger.error(f"Index file '{relative_path}' does not exist in '{self.fingerprint_dir}'.")
print(colored(f"Error: Index file '{relative_path}' does not exist.", 'red'))
return None
with lock_file(self.fingerprint_dir / relative_path, fcntl.LOCK_SH):
with open(self.fingerprint_dir / relative_path, 'rb') as file:
encrypted_data = file.read()
logger.debug(f"Encrypted index data read from '{INDEX_FILE}'.")
logger.debug(f"Encrypted index data read from '{relative_path}'.")
return encrypted_data
except Exception as e:
logger.error(f"Failed to read encrypted index file '{INDEX_FILE}': {e}")
logger.error(f"Failed to read encrypted index file '{relative_path}': {e}")
logger.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to read encrypted index file '{INDEX_FILE}': {e}", 'red'))
print(colored(f"Error: Failed to read encrypted index file '{relative_path}': {e}", 'red'))
return None
def decrypt_and_save_index_from_nostr(self, encrypted_data: bytes) -> None:
def decrypt_and_save_index_from_nostr(self, encrypted_data: bytes, relative_path: Optional[Path] = None) -> None:
"""
Decrypts the encrypted data retrieved from Nostr and updates the local index file.
:param encrypted_data: The encrypted data retrieved from Nostr.
:param relative_path: The relative path within the fingerprint directory to update.
Defaults to 'seedpass_passwords_db.json.enc'.
"""
if relative_path is None:
relative_path = Path('seedpass_passwords_db.json.enc')
try:
decrypted_data = self.decrypt_data(encrypted_data)
data = json.loads(decrypted_data.decode('utf-8'))
self.save_json_data(data, INDEX_FILE)
self.update_checksum(INDEX_FILE)
self.save_json_data(data, relative_path)
self.update_checksum(relative_path)
logger.info("Index file updated from Nostr successfully.")
print(colored("Index file updated from Nostr successfully.", 'green'))
except Exception as e:
logger.error(f"Failed to decrypt and save data from Nostr: {e}")
logger.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to decrypt and save data from Nostr: {e}", 'red'))
def decrypt_parent_seed(self, file_path: Path) -> str:
"""
Decrypts and retrieves the parent seed from the specified file.
:param file_path: The path to the file containing the encrypted parent seed.
:return: The decrypted parent seed as a string.
"""
try:
decrypted_data = self.decrypt_file(file_path)
parent_seed = decrypted_data.decode('utf-8').strip()
logger.debug(f"Decrypted parent_seed: {parent_seed} (Type: {type(parent_seed)})")
return parent_seed
except Exception as e:
logger.error(f"Failed to decrypt parent seed from '{file_path}': {e}")
logger.error(traceback.format_exc())
print(colored(f"Error: Failed to decrypt parent seed from '{file_path}': {e}", 'red'))
print(colored(f"Error: Failed to decrypt and save data from Nostr: {e}", 'red'))
# Re-raise the exception to inform the calling function of the failure
raise
def validate_seed(self, seed_phrase: str) -> bool:
@@ -343,17 +368,17 @@ class EncryptionManager:
:return: True if valid, False otherwise.
"""
try:
mnemo = Mnemonic("english")
is_valid = mnemo.check(seed_phrase)
if not is_valid:
logger.error("Invalid BIP39 seed phrase.")
print(colored("Error: Invalid BIP39 seed phrase.", 'red'))
else:
logger.debug("BIP39 seed phrase validated successfully.")
return is_valid
words = seed_phrase.split()
if len(words) != 12:
logger.error("Seed phrase does not contain exactly 12 words.")
print(colored("Error: Seed phrase must contain exactly 12 words.", 'red'))
return False
# Additional validation can be added here (e.g., word list checks)
logger.debug("Seed phrase validated successfully.")
return True
except Exception as e:
logger.error(f"Error validating seed phrase: {e}")
logger.error(traceback.format_exc()) # Log full traceback
logging.error(f"Error validating seed phrase: {e}")
logging.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to validate seed phrase: {e}", 'red'))
return False
@@ -373,12 +398,12 @@ class EncryptionManager:
mnemonic = str(mnemonic)
if not isinstance(mnemonic, str):
raise TypeError("Mnemonic must be a string after conversion")
mnemo = Mnemonic("english")
seed = mnemo.to_seed(mnemonic, passphrase)
from bip_utils import Bip39SeedGenerator
seed = Bip39SeedGenerator(mnemonic).Generate(passphrase)
logger.debug("Seed derived successfully from mnemonic.")
return seed
except Exception as e:
logger.error(f"Failed to derive seed from mnemonic: {e}")
logger.error(traceback.format_exc())
print(f"Error: Failed to derive seed from mnemonic: {e}")
print(colored(f"Error: Failed to derive seed from mnemonic: {e}", 'red'))
raise

View File

@@ -10,8 +10,9 @@ that all changes are securely encrypted and properly indexed.
Ensure that all dependencies are installed and properly configured in your environment.
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.
Never use or suggest using Random Salt. The purpose of this password manager is to derive
completely deterministic passwords from a BIP-85 seed, ensuring that passwords are generated
the same way every time. Salts would break this functionality and are not suitable for this software.
"""
import json
@@ -23,80 +24,56 @@ import shutil
import time
import traceback
from typing import Optional, Tuple, Dict, Any, List
from pathlib import Path
from colorama import Fore
from termcolor import colored
from password_manager.encryption import EncryptionManager
from constants import INDEX_FILE, DATA_CHECKSUM_FILE
from utils.file_lock import lock_file
import fcntl # Required for lock_type constants in lock_file
# Configure logging at the start of the module
def configure_logging():
"""
Configures logging with both file and console handlers.
Logs include the timestamp, log level, message, filename, and line number.
Only ERROR and higher-level messages are shown in the terminal, while all messages
are logged in the log file.
"""
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG) # Set to DEBUG for detailed output
# Prevent adding multiple handlers if configure_logging is called multiple times
if not logger.handlers:
# 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', 'entry_management.log'))
# 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
formatter = logging.Formatter(
'%(asctime)s [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]'
)
c_handler.setFormatter(formatter)
f_handler.setFormatter(formatter)
# Add handlers to the logger
logger.addHandler(c_handler)
logger.addHandler(f_handler)
# Call the logging configuration function
configure_logging()
import fcntl
# Instantiate the logger
logger = logging.getLogger(__name__)
class EntryManager:
"""
EntryManager Class
Handles the creation, retrieval, modification, and listing of password entries
within the encrypted password index. It ensures that all operations are performed
securely, maintaining data integrity and confidentiality.
"""
def __init__(self, encryption_manager: EncryptionManager):
def __init__(self, encryption_manager: EncryptionManager, fingerprint_dir: Path):
"""
Initializes the EntryManager with an instance of EncryptionManager.
Initializes the EntryManager with the EncryptionManager and fingerprint directory.
:param encryption_manager: An instance of EncryptionManager for handling encryption.
:param encryption_manager: The encryption manager instance.
:param fingerprint_dir: The directory corresponding to the fingerprint.
"""
self.encryption_manager = encryption_manager
self.fingerprint_dir = fingerprint_dir
# Use paths relative to the fingerprint directory
self.index_file = self.fingerprint_dir / 'seedpass_passwords_db.json.enc'
self.checksum_file = self.fingerprint_dir / 'seedpass_passwords_db_checksum.txt'
logger.debug(f"EntryManager initialized with index file at {self.index_file}")
def _load_index(self) -> Dict[str, Any]:
if self.index_file.exists():
try:
data = self.encryption_manager.load_json_data(self.index_file)
logger.debug("Index loaded successfully.")
return data
except Exception as e:
logger.error(f"Failed to load index: {e}")
return {'passwords': {}}
else:
logger.info(f"Index file '{self.index_file}' not found. Initializing new password database.")
return {'passwords': {}}
def _save_index(self, data: Dict[str, Any]) -> None:
try:
self.encryption_manager = encryption_manager
logger.debug("EntryManager initialized with provided EncryptionManager.")
self.encryption_manager.save_json_data(data, self.index_file)
logger.debug("Index saved successfully.")
except Exception as e:
logger.error(f"Failed to initialize EntryManager: {e}")
logger.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to initialize EntryManager: {e}", 'red'))
sys.exit(1)
logger.error(f"Failed to save index: {e}")
raise
def get_next_index(self) -> int:
"""
@@ -105,7 +82,7 @@ class EntryManager:
:return: The next index number as an integer.
"""
try:
data = self.encryption_manager.load_json_data()
data = self.encryption_manager.load_json_data(self.index_file)
if 'passwords' in data and isinstance(data['passwords'], dict):
indices = [int(idx) for idx in data['passwords'].keys()]
next_index = max(indices) + 1 if indices else 0
@@ -115,7 +92,7 @@ class EntryManager:
return next_index
except Exception as e:
logger.error(f"Error determining next index: {e}")
logger.error(traceback.format_exc()) # Log full traceback
logger.error(traceback.format_exc())
print(colored(f"Error determining next index: {e}", 'red'))
sys.exit(1)
@@ -133,11 +110,7 @@ class EntryManager:
"""
try:
index = self.get_next_index()
data = self.encryption_manager.load_json_data()
if 'passwords' not in data or not isinstance(data['passwords'], dict):
data['passwords'] = {}
logger.debug("'passwords' key was missing. Initialized empty 'passwords' dictionary.")
data = self.encryption_manager.load_json_data(self.index_file)
data['passwords'][str(index)] = {
'website': website_name,
@@ -149,7 +122,7 @@ class EntryManager:
logger.debug(f"Added entry at index {index}: {data['passwords'][str(index)]}")
self.encryption_manager.save_json_data(data)
self._save_index(data)
self.update_checksum()
self.backup_index_file()
@@ -160,10 +133,32 @@ class EntryManager:
except Exception as e:
logger.error(f"Failed to add entry: {e}")
logger.error(traceback.format_exc()) # Log full traceback
logger.error(traceback.format_exc())
print(colored(f"Error: Failed to add entry: {e}", 'red'))
sys.exit(1)
def get_encrypted_index(self) -> Optional[bytes]:
"""
Retrieves the encrypted password index file's contents.
:return: The encrypted data as bytes, or None if retrieval fails.
"""
try:
if not self.index_file.exists():
logger.error(f"Index file '{self.index_file}' does not exist.")
print(colored(f"Error: Index file '{self.index_file}' does not exist.", 'red'))
return None
with open(self.index_file, 'rb') as file:
encrypted_data = file.read()
logger.debug("Encrypted index file data retrieved successfully.")
return encrypted_data
except Exception as e:
logger.error(f"Failed to retrieve encrypted index file: {e}")
logger.error(traceback.format_exc())
print(colored(f"Error: Failed to retrieve encrypted index file: {e}", 'red'))
return None
def retrieve_entry(self, index: int) -> Optional[Dict[str, Any]]:
"""
Retrieves a password entry based on the provided index.
@@ -172,7 +167,7 @@ class EntryManager:
:return: A dictionary containing the entry details or None if not found.
"""
try:
data = self.encryption_manager.load_json_data()
data = self.encryption_manager.load_json_data(self.index_file)
entry = data.get('passwords', {}).get(str(index))
if entry:
@@ -185,7 +180,7 @@ class EntryManager:
except Exception as e:
logger.error(f"Failed to retrieve entry at index {index}: {e}")
logger.error(traceback.format_exc()) # Log full traceback
logger.error(traceback.format_exc())
print(colored(f"Error: Failed to retrieve entry at index {index}: {e}", 'red'))
return None
@@ -201,7 +196,7 @@ class EntryManager:
:param blacklisted: (Optional) The new blacklist status.
"""
try:
data = self.encryption_manager.load_json_data()
data = self.encryption_manager.load_json_data(self.index_file)
entry = data.get('passwords', {}).get(str(index))
if not entry:
@@ -224,7 +219,7 @@ class EntryManager:
data['passwords'][str(index)] = entry
logger.debug(f"Modified entry at index {index}: {entry}")
self.encryption_manager.save_json_data(data)
self._save_index(data)
self.update_checksum()
self.backup_index_file()
@@ -233,7 +228,7 @@ class EntryManager:
except Exception as e:
logger.error(f"Failed to modify entry at index {index}: {e}")
logger.error(traceback.format_exc()) # Log full traceback
logger.error(traceback.format_exc())
print(colored(f"Error: Failed to modify entry at index {index}: {e}", 'red'))
def list_entries(self) -> List[Tuple[int, str, Optional[str], Optional[str], bool]]:
@@ -308,14 +303,17 @@ class EntryManager:
Updates the checksum file for the password database to ensure data integrity.
"""
try:
data = self.encryption_manager.load_json_data()
data = self.encryption_manager.load_json_data(self.index_file)
json_content = json.dumps(data, indent=4)
checksum = hashlib.sha256(json_content.encode('utf-8')).hexdigest()
with open(DATA_CHECKSUM_FILE, 'w') as f:
# Construct the full path for the checksum file
checksum_path = self.fingerprint_dir / self.checksum_file
with open(checksum_path, 'w') as f:
f.write(checksum)
logger.debug(f"Checksum updated and written to '{DATA_CHECKSUM_FILE}'.")
logger.debug(f"Checksum updated and written to '{checksum_path}'.")
print(colored(f"[+] Checksum updated successfully.", 'green'))
except Exception as e:
@@ -328,15 +326,16 @@ class EntryManager:
Creates a backup of the encrypted JSON index file to prevent data loss.
"""
try:
if not os.path.exists(INDEX_FILE):
logger.warning(f"Index file '{INDEX_FILE}' does not exist. No backup created.")
index_file_path = self.fingerprint_dir / self.index_file
if not index_file_path.exists():
logger.warning(f"Index file '{index_file_path}' does not exist. No backup created.")
return
timestamp = int(time.time())
backup_filename = f'passwords_db_backup_{timestamp}.json.enc'
backup_path = os.path.join(os.path.dirname(INDEX_FILE), backup_filename)
backup_path = self.fingerprint_dir / backup_filename
with open(INDEX_FILE, 'rb') as original_file, open(backup_path, 'wb') as backup_file:
with open(index_file_path, 'rb') as original_file, open(backup_path, 'wb') as backup_file:
shutil.copyfileobj(original_file, backup_file)
logger.debug(f"Backup created at '{backup_path}'.")
@@ -347,6 +346,7 @@ class EntryManager:
logger.error(traceback.format_exc()) # Log full traceback
print(colored(f"Warning: Failed to create backup: {e}", 'yellow'))
def restore_from_backup(self, backup_path: str) -> None:
"""
Restores the index file from a specified backup file.
@@ -359,7 +359,7 @@ class EntryManager:
print(colored(f"Error: Backup file '{backup_path}' does not exist.", 'red'))
return
with open(backup_path, 'rb') as backup_file, open(INDEX_FILE, 'wb') as index_file:
with open(backup_path, 'rb') as backup_file, open(self.index_file, 'wb') as index_file:
shutil.copyfileobj(backup_file, index_file)
logger.debug(f"Index file restored from backup '{backup_path}'.")

View File

@@ -15,7 +15,7 @@ import logging
import getpass
import os
from typing import Optional
import shutil
from colorama import Fore
from termcolor import colored
@@ -29,67 +29,27 @@ from utils.password_prompt import prompt_for_password, prompt_existing_password,
from constants import (
APP_DIR,
INDEX_FILE,
PARENT_SEED_FILE,
DATA_CHECKSUM_FILE,
SCRIPT_CHECKSUM_FILE,
MIN_PASSWORD_LENGTH,
MAX_PASSWORD_LENGTH,
DEFAULT_PASSWORD_LENGTH,
HASHED_PASSWORD_FILE, # Ensure this constant is defined in constants.py
DEFAULT_SEED_BACKUP_FILENAME
)
import traceback # Added for exception traceback logging
import bcrypt # Ensure bcrypt is installed in your environment
from pathlib import Path # Required for handling file paths
import traceback
import bcrypt
from pathlib import Path
from bip85.bip85 import BIP85
from local_bip85.bip85 import BIP85
from bip_utils import Bip39SeedGenerator, Bip39MnemonicGenerator, Bip39Languages
# Import NostrClient from the nostr package
from nostr import NostrClient # <-- Added import statement
from utils.fingerprint_manager import FingerprintManager
# Configure logging at the start of the module
def configure_logging():
"""
Configures logging with both file and console handlers.
Logs include the timestamp, log level, message, filename, and line number.
Only ERROR and higher-level messages are shown in the terminal, while all messages
are logged in the log file.
"""
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG) # Set to DEBUG for detailed output
# Import NostrClient
from nostr.client import NostrClient
# Prevent adding multiple handlers if configure_logging is called multiple times
if not logger.handlers:
# 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_manager.log'))
# Set levels: only errors and critical messages will be shown in the console
c_handler.setLevel(logging.ERROR)
f_handler.setLevel(logging.DEBUG)
# Create formatters and add them to handlers, include file and line number in log messages
formatter = logging.Formatter(
'%(asctime)s [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]'
)
c_handler.setFormatter(formatter)
f_handler.setFormatter(formatter)
# Add handlers to the logger
logger.addHandler(c_handler)
logger.addHandler(f_handler)
# Call the logging configuration function
configure_logging()
# Initialize the logger for this module
# Instantiate the logger
logger = logging.getLogger(__name__)
class PasswordManager:
@@ -104,48 +64,286 @@ class PasswordManager:
def __init__(self):
"""
Initializes the PasswordManager by setting up encryption, loading or setting up the parent seed,
and initializing other components like EntryManager, PasswordGenerator, and BackupManager.
and initializing other components like EntryManager, PasswordGenerator, BackupManager, and FingerprintManager.
"""
self.encryption_manager: Optional[EncryptionManager] = None
self.entry_manager: Optional[EntryManager] = None
self.password_generator: Optional[PasswordGenerator] = None
self.backup_manager: Optional[BackupManager] = None
self.parent_seed: Optional[str] = None # Ensured to be a string
self.bip85: Optional[BIP85] = None # Added bip85 attribute
self.fingerprint_manager: Optional[FingerprintManager] = None
self.parent_seed: Optional[str] = None
self.bip85: Optional[BIP85] = None
self.nostr_client: Optional[NostrClient] = None
# Initialize the fingerprint manager first
self.initialize_fingerprint_manager()
# Ensure a parent seed is set up before accessing the fingerprint directory
self.setup_parent_seed()
self.initialize_managers()
# Set the current fingerprint directory
self.fingerprint_dir = self.fingerprint_manager.get_current_fingerprint_dir()
def initialize_fingerprint_manager(self):
"""
Initializes the FingerprintManager.
"""
try:
self.fingerprint_manager = FingerprintManager(APP_DIR)
logger.debug("FingerprintManager initialized successfully.")
except Exception as e:
logger.error(f"Failed to initialize FingerprintManager: {e}")
logger.error(traceback.format_exc())
print(colored(f"Error: Failed to initialize FingerprintManager: {e}", 'red'))
sys.exit(1)
def setup_parent_seed(self) -> None:
"""
Sets up the parent seed by determining if an existing seed is present or if a new one needs to be created.
Sets up the parent seed by determining if existing fingerprints are present or if a new one needs to be created.
"""
if os.path.exists(PARENT_SEED_FILE):
self.handle_existing_seed()
fingerprints = self.fingerprint_manager.list_fingerprints()
if fingerprints:
# There are existing fingerprints
self.select_or_add_fingerprint()
else:
# No existing fingerprints, proceed to set up new seed
self.handle_new_seed_setup()
def select_or_add_fingerprint(self):
"""
Prompts the user to select an existing fingerprint or add a new one.
"""
try:
print(colored("\nAvailable Fingerprints:", 'cyan'))
fingerprints = self.fingerprint_manager.list_fingerprints()
for idx, fp in enumerate(fingerprints, start=1):
print(colored(f"{idx}. {fp}", 'cyan'))
print(colored(f"{len(fingerprints)+1}. Add a new fingerprint", 'cyan'))
choice = input("Select a fingerprint by number: ").strip()
if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints)+1):
print(colored("Invalid selection. Exiting.", 'red'))
sys.exit(1)
choice = int(choice)
if choice == len(fingerprints)+1:
# Add a new fingerprint
self.add_new_fingerprint()
else:
# Select existing fingerprint
selected_fingerprint = fingerprints[choice-1]
self.select_fingerprint(selected_fingerprint)
except Exception as e:
logger.error(f"Error during fingerprint selection: {e}")
logger.error(traceback.format_exc())
print(colored(f"Error: Failed to select fingerprint: {e}", 'red'))
sys.exit(1)
def add_new_fingerprint(self):
"""
Adds a new fingerprint by generating it from a seed phrase.
"""
try:
choice = input("Do you want to (1) Enter an existing seed or (2) Generate a new seed? (1/2): ").strip()
if choice == '1':
fingerprint = self.setup_existing_seed()
elif choice == '2':
fingerprint = self.generate_new_seed()
else:
print(colored("Invalid choice. Exiting.", 'red'))
sys.exit(1)
# Set current_fingerprint in FingerprintManager only
self.fingerprint_manager.current_fingerprint = fingerprint
print(colored(f"New fingerprint '{fingerprint}' added and set as current.", 'green'))
except Exception as e:
logger.error(f"Error adding new fingerprint: {e}")
logger.error(traceback.format_exc())
print(colored(f"Error: Failed to add new fingerprint: {e}", 'red'))
sys.exit(1)
def select_fingerprint(self, fingerprint: str) -> None:
if self.fingerprint_manager.select_fingerprint(fingerprint):
self.current_fingerprint = fingerprint # Add this line
self.fingerprint_dir = self.fingerprint_manager.get_current_fingerprint_dir()
if not self.fingerprint_dir:
print(colored(f"Error: Fingerprint directory for {fingerprint} not found.", 'red'))
sys.exit(1)
# Setup the encryption manager and load parent seed
self.setup_encryption_manager(self.fingerprint_dir)
self.load_parent_seed(self.fingerprint_dir)
# Initialize BIP85 and other managers
self.initialize_bip85()
self.initialize_managers()
print(colored(f"Fingerprint {fingerprint} selected and managers initialized.", 'green'))
else:
print(colored(f"Error: Fingerprint {fingerprint} not found.", 'red'))
sys.exit(1)
def setup_encryption_manager(self, fingerprint_dir: Path, password: Optional[str] = None):
"""
Sets up the EncryptionManager for the selected fingerprint.
Parameters:
fingerprint_dir (Path): The directory corresponding to the fingerprint.
password (Optional[str]): The user's master password.
"""
try:
# Prompt for password if not provided
if password is None:
password = prompt_existing_password("Enter your master password: ")
# Derive key from password
key = derive_key_from_password(password)
self.encryption_manager = EncryptionManager(key, fingerprint_dir)
logger.debug("EncryptionManager set up successfully for selected fingerprint.")
# Verify the password
self.fingerprint_dir = fingerprint_dir # Ensure self.fingerprint_dir is set
if not self.verify_password(password):
print(colored("Invalid password. Exiting.", 'red'))
sys.exit(1)
except Exception as e:
logger.error(f"Failed to set up EncryptionManager: {e}")
logger.error(traceback.format_exc())
print(colored(f"Error: Failed to set up encryption: {e}", 'red'))
sys.exit(1)
def load_parent_seed(self, fingerprint_dir: Path):
"""
Loads and decrypts the parent seed from the fingerprint directory.
Parameters:
fingerprint_dir (Path): The directory corresponding to the fingerprint.
"""
try:
self.parent_seed = self.encryption_manager.decrypt_parent_seed()
logger.debug(f"Parent seed loaded for fingerprint {self.current_fingerprint}.")
# Initialize BIP85 with the parent seed
seed_bytes = Bip39SeedGenerator(self.parent_seed).Generate()
self.bip85 = BIP85(seed_bytes)
logger.debug("BIP-85 initialized successfully.")
except Exception as e:
logger.error(f"Failed to load parent seed: {e}")
logger.error(traceback.format_exc())
print(colored(f"Error: Failed to load parent seed: {e}", 'red'))
sys.exit(1)
def handle_switch_fingerprint(self) -> bool:
"""
Handles switching to a different fingerprint.
Returns:
bool: True if switch was successful, False otherwise.
"""
try:
print(colored("\nAvailable Fingerprints:", 'cyan'))
fingerprints = self.fingerprint_manager.list_fingerprints()
for idx, fp in enumerate(fingerprints, start=1):
print(colored(f"{idx}. {fp}", 'cyan'))
choice = input("Select a fingerprint by number to switch: ").strip()
if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints)):
print(colored("Invalid selection. Returning to main menu.", 'red'))
return False # Return False to indicate failure
selected_fingerprint = fingerprints[int(choice) - 1]
self.fingerprint_manager.current_fingerprint = selected_fingerprint
self.current_fingerprint = selected_fingerprint
# Update fingerprint directory
self.fingerprint_dir = self.fingerprint_manager.get_current_fingerprint_dir()
if not self.fingerprint_dir:
print(colored(f"Error: Fingerprint directory for {selected_fingerprint} not found.", 'red'))
return False # Return False to indicate failure
# Prompt for master password for the selected fingerprint
password = prompt_existing_password("Enter your master password: ")
# Set up the encryption manager with the new password and fingerprint directory
self.setup_encryption_manager(self.fingerprint_dir, password)
# Load the parent seed for the selected fingerprint
self.load_parent_seed(self.fingerprint_dir)
# Initialize BIP85 and other managers
self.initialize_bip85()
self.initialize_managers()
print(colored(f"Switched to fingerprint {selected_fingerprint}.", 'green'))
# Re-initialize NostrClient with the new fingerprint
try:
self.nostr_client = NostrClient(
encryption_manager=self.encryption_manager,
fingerprint=self.current_fingerprint
)
logging.info(f"NostrClient re-initialized with fingerprint {self.current_fingerprint}.")
except Exception as e:
logging.error(f"Failed to re-initialize NostrClient: {e}")
print(colored(f"Error: Failed to re-initialize NostrClient: {e}", 'red'))
return False
return True # Return True to indicate success
except Exception as e:
logging.error(f"Error during fingerprint switching: {e}")
logging.error(traceback.format_exc())
print(colored(f"Error: Failed to switch fingerprints: {e}", 'red'))
return False # Return False to indicate failure
def handle_existing_seed(self) -> None:
"""
Handles the scenario where an existing parent seed file is found.
Prompts the user for the master password to decrypt the seed.
"""
password = getpass.getpass(prompt='Enter your login password: ').strip()
try:
# Prompt for password
password = getpass.getpass(prompt='Enter your login password: ').strip()
# Derive encryption key from password
key = derive_key_from_password(password)
self.encryption_manager = EncryptionManager(key)
self.parent_seed = self.encryption_manager.decrypt_parent_seed(PARENT_SEED_FILE)
# Initialize FingerprintManager if not already initialized
if not self.fingerprint_manager:
self.initialize_fingerprint_manager()
# Prompt the user to select an existing fingerprint
fingerprints = self.fingerprint_manager.list_fingerprints()
if not fingerprints:
print(colored("No fingerprints available. Please add a fingerprint first.", 'red'))
sys.exit(1)
print(colored("Available Fingerprints:", 'cyan'))
for idx, fp in enumerate(fingerprints, start=1):
print(colored(f"{idx}. {fp}", 'cyan'))
choice = input("Select a fingerprint by number: ").strip()
if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints)):
print(colored("Invalid selection. Exiting.", 'red'))
sys.exit(1)
selected_fingerprint = fingerprints[int(choice)-1]
self.current_fingerprint = selected_fingerprint
fingerprint_dir = self.fingerprint_manager.get_fingerprint_directory(selected_fingerprint)
if not fingerprint_dir:
print(colored("Error: Fingerprint directory not found.", 'red'))
sys.exit(1)
# Initialize EncryptionManager with key and fingerprint_dir
self.encryption_manager = EncryptionManager(key, fingerprint_dir)
self.parent_seed = self.encryption_manager.decrypt_parent_seed()
# Log the type and content of parent_seed
logger.debug(f"Decrypted parent_seed: {self.parent_seed} (type: {type(self.parent_seed)})")
# Validate the decrypted seed
if not self.validate_bip85_seed(self.parent_seed):
logging.error("Decrypted seed is invalid. Exiting.")
print(colored("Error: Decrypted seed is invalid.", 'red'))
sys.exit(1)
self.initialize_bip85()
logging.debug("Parent seed decrypted and validated successfully.")
except Exception as e:
@@ -170,25 +368,67 @@ class PasswordManager:
print(colored("Invalid choice. Exiting.", 'red'))
sys.exit(1)
def setup_existing_seed(self) -> None:
def setup_existing_seed(self) -> Optional[str]:
"""
Prompts the user to enter an existing BIP-85 seed and validates it.
Returns:
Optional[str]: The fingerprint if setup is successful, None otherwise.
"""
try:
parent_seed = getpass.getpass(prompt='Enter your 12-word BIP-85 seed: ').strip()
if self.validate_bip85_seed(parent_seed):
self.save_and_encrypt_seed(parent_seed)
# Add a fingerprint using the existing seed
fingerprint = self.fingerprint_manager.add_fingerprint(parent_seed)
if not fingerprint:
print(colored("Error: Failed to generate fingerprint for the provided seed.", 'red'))
sys.exit(1)
fingerprint_dir = self.fingerprint_manager.get_fingerprint_directory(fingerprint)
if not fingerprint_dir:
print(colored("Error: Failed to retrieve fingerprint directory.", 'red'))
sys.exit(1)
# Set the current fingerprint in both PasswordManager and FingerprintManager
self.current_fingerprint = fingerprint
self.fingerprint_manager.current_fingerprint = fingerprint
self.fingerprint_dir = fingerprint_dir
logging.info(f"Current fingerprint set to {fingerprint}")
# Initialize EncryptionManager with key and fingerprint_dir
password = prompt_for_password()
key = derive_key_from_password(password)
self.encryption_manager = EncryptionManager(key, fingerprint_dir)
# Encrypt and save the parent seed
self.encryption_manager.encrypt_parent_seed(parent_seed)
logging.info("Parent seed encrypted and saved successfully.")
# Store the hashed password
self.store_hashed_password(password)
logging.info("User password hashed and stored successfully.")
self.parent_seed = parent_seed # Ensure this is a string
logger.debug(f"parent_seed set to: {self.parent_seed} (type: {type(self.parent_seed)})")
self.initialize_bip85()
self.initialize_managers()
return fingerprint # Return the generated or added fingerprint
else:
logging.error("Invalid BIP-85 seed phrase. Exiting.")
print(colored("Error: Invalid BIP-85 seed phrase.", 'red'))
sys.exit(1)
except KeyboardInterrupt:
logging.info("Operation cancelled by user.")
print(colored("\nOperation cancelled by user.", 'yellow'))
sys.exit(0)
def generate_new_seed(self) -> None:
def generate_new_seed(self) -> Optional[str]:
"""
Generates a new BIP-85 seed, displays it to the user, and prompts for confirmation before saving.
Returns:
Optional[str]: The fingerprint if generation is successful, None otherwise.
"""
new_seed = self.generate_bip85_seed()
print(colored("Your new BIP-85 seed phrase is:", 'green'))
@@ -196,7 +436,26 @@ class PasswordManager:
print(colored("Please write this down and keep it in a safe place!", 'red'))
if confirm_action("Do you want to use this generated seed? (Y/N): "):
self.save_and_encrypt_seed(new_seed)
# Add a new fingerprint using the generated seed
fingerprint = self.fingerprint_manager.add_fingerprint(new_seed)
if not fingerprint:
print(colored("Error: Failed to generate fingerprint for the new seed.", 'red'))
sys.exit(1)
fingerprint_dir = self.fingerprint_manager.get_fingerprint_directory(fingerprint)
if not fingerprint_dir:
print(colored("Error: Failed to retrieve fingerprint directory.", 'red'))
sys.exit(1)
# Set the current fingerprint in both PasswordManager and FingerprintManager
self.current_fingerprint = fingerprint
self.fingerprint_manager.current_fingerprint = fingerprint
logging.info(f"Current fingerprint set to {fingerprint}")
# Now, save and encrypt the seed with the fingerprint_dir
self.save_and_encrypt_seed(new_seed, fingerprint_dir)
return fingerprint # Return the generated fingerprint
else:
print(colored("Seed generation cancelled. Exiting.", 'yellow'))
sys.exit(0)
@@ -231,7 +490,7 @@ class PasswordManager:
try:
master_seed = os.urandom(32) # Generate a random 32-byte seed
bip85 = BIP85(master_seed)
mnemonic_obj = bip85.derive_mnemonic(app_no=39, language_code=0, words_num=12, index=0)
mnemonic_obj = bip85.derive_mnemonic(index=0, words_num=12)
mnemonic_str = mnemonic_obj.ToStr() # Convert Bip39Mnemonic object to string
return mnemonic_str
except Exception as e:
@@ -240,28 +499,38 @@ class PasswordManager:
print(colored(f"Error: Failed to generate BIP-85 seed: {e}", 'red'))
sys.exit(1)
def save_and_encrypt_seed(self, seed: str) -> None:
def save_and_encrypt_seed(self, seed: str, fingerprint_dir: Path) -> None:
"""
Saves and encrypts the parent seed.
Parameters:
seed (str): The BIP-85 seed phrase to save and encrypt.
fingerprint_dir (Path): The directory corresponding to the fingerprint.
"""
password = prompt_for_password()
key = derive_key_from_password(password)
self.encryption_manager = EncryptionManager(key)
try:
self.encryption_manager.encrypt_parent_seed(seed, PARENT_SEED_FILE)
logging.info("Parent seed encrypted and saved successfully.")
# Set self.fingerprint_dir
self.fingerprint_dir = fingerprint_dir
# Prompt for password
password = prompt_for_password()
# Derive key from password
key = derive_key_from_password(password)
# Re-initialize EncryptionManager with the new key and fingerprint_dir
self.encryption_manager = EncryptionManager(key, fingerprint_dir)
# Store the hashed password
self.store_hashed_password(password)
logging.info("User password hashed and stored successfully.")
# Encrypt and save the parent seed
self.encryption_manager.encrypt_parent_seed(seed)
logging.info("Parent seed encrypted and saved successfully.")
self.parent_seed = seed # Ensure this is a string
logger.debug(f"parent_seed set to: {self.parent_seed} (type: {type(self.parent_seed)})")
self.initialize_bip85()
self.initialize_managers()
except Exception as e:
logging.error(f"Failed to encrypt and save parent seed: {e}")
logging.error(traceback.format_exc())
@@ -284,20 +553,38 @@ class PasswordManager:
def initialize_managers(self) -> None:
"""
Initializes the EntryManager, PasswordGenerator, and BackupManager with the EncryptionManager
and BIP-85 instance.
Initializes the EntryManager, PasswordGenerator, BackupManager, and NostrClient with the EncryptionManager
and BIP-85 instance within the context of the selected fingerprint.
"""
try:
self.entry_manager = EntryManager(self.encryption_manager)
self.password_generator = PasswordGenerator(self.encryption_manager, self.parent_seed)
self.backup_manager = BackupManager()
# Ensure self.encryption_manager is already initialized
if not self.encryption_manager:
raise ValueError("EncryptionManager is not initialized.")
# Directly pass the parent_seed string to NostrClient
self.nostr_client = NostrClient(parent_seed=self.parent_seed) # <-- NostrClient is now imported
# Reinitialize the managers with the updated EncryptionManager and current fingerprint context
self.entry_manager = EntryManager(
encryption_manager=self.encryption_manager,
fingerprint_dir=self.fingerprint_dir
)
self.password_generator = PasswordGenerator(
encryption_manager=self.encryption_manager,
parent_seed=self.parent_seed,
bip85=self.bip85
)
self.backup_manager = BackupManager(fingerprint_dir=self.fingerprint_dir)
logging.debug("EntryManager, PasswordGenerator, BackupManager, and NostrClient initialized.")
# Initialize the NostrClient with the current fingerprint
self.nostr_client = NostrClient(
encryption_manager=self.encryption_manager,
fingerprint=self.current_fingerprint # Pass the current fingerprint
)
logger.debug("Managers re-initialized for the new fingerprint.")
except Exception as e:
logging.error(f"Failed to initialize managers: {e}")
logger.error(f"Failed to initialize managers: {e}")
logging.error(traceback.format_exc())
print(colored(f"Error: Failed to initialize managers: {e}", 'red'))
sys.exit(1)
@@ -464,7 +751,7 @@ class PasswordManager:
:return: The encrypted data as bytes, or None if retrieval fails.
"""
try:
encrypted_data = self.encryption_manager.get_encrypted_index()
encrypted_data = self.entry_manager.get_encrypted_index()
if encrypted_data:
logging.debug("Encrypted index data retrieved successfully.")
return encrypted_data
@@ -485,13 +772,22 @@ class PasswordManager:
:param encrypted_data: The encrypted data retrieved from Nostr.
"""
try:
self.encryption_manager.decrypt_and_save_index_from_nostr(encrypted_data)
# Decrypt the data using EncryptionManager's decrypt_data method
decrypted_data = self.encryption_manager.decrypt_data(encrypted_data)
# Save the decrypted data to the index file
index_file_path = self.fingerprint_dir / 'seedpass_passwords_db.json.enc'
with open(index_file_path, 'wb') as f:
f.write(decrypted_data)
logging.info("Index file updated from Nostr successfully.")
print(colored("Index file updated from Nostr successfully.", 'green'))
except Exception as e:
logging.error(f"Failed to decrypt and save data from Nostr: {e}")
logging.error(traceback.format_exc())
print(colored(f"Error: Failed to decrypt and save data from Nostr: {e}", 'red'))
# Re-raise the exception to inform the calling function of the failure
raise
def backup_database(self) -> None:
"""
@@ -546,14 +842,15 @@ class PasswordManager:
if confirm_action("Do you want to save this to an encrypted backup file? (Y/N): "):
filename = input(f"Enter filename to save (default: {DEFAULT_SEED_BACKUP_FILENAME}): ").strip()
filename = filename if filename else DEFAULT_SEED_BACKUP_FILENAME
backup_path = Path(APP_DIR) / filename
backup_path = self.fingerprint_dir / filename # Save in fingerprint directory
# Validate filename
if not self.is_valid_filename(filename):
print(colored("Invalid filename. Operation aborted.", 'red'))
return
self.encryption_manager.encrypt_parent_seed(self.parent_seed, backup_path)
# Encrypt and save the parent seed to the backup path
self.encryption_manager.encrypt_and_save_file(self.parent_seed.encode('utf-8'), backup_path)
print(colored(f"Encrypted seed backup saved to '{backup_path}'. Ensure this file is stored securely.", 'green'))
except Exception as e:
@@ -572,11 +869,12 @@ class PasswordManager:
bool: True if the password is correct, False otherwise.
"""
try:
if not os.path.exists(HASHED_PASSWORD_FILE):
hashed_password_file = self.fingerprint_dir / 'hashed_password.enc'
if not hashed_password_file.exists():
logging.error("Hashed password file not found.")
print(colored("Error: Hashed password file not found.", 'red'))
return False
with open(HASHED_PASSWORD_FILE, 'rb') as f:
with open(hashed_password_file, 'rb') as f:
stored_hash = f.read()
is_correct = bcrypt.checkpw(password.encode('utf-8'), stored_hash)
if is_correct:
@@ -613,19 +911,19 @@ class PasswordManager:
This should be called during the initial setup.
"""
try:
hashed_password_file = self.fingerprint_dir / 'hashed_password.enc'
hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
with open(HASHED_PASSWORD_FILE, 'wb') as f:
with open(hashed_password_file, 'wb') as f:
f.write(hashed)
# Set file permissions to read/write for the user only
os.chmod(HASHED_PASSWORD_FILE, 0o600)
os.chmod(hashed_password_file, 0o600)
logging.info("User password hashed and stored successfully.")
except AttributeError:
# If bcrypt.hashpw is not available, try using bcrypt directly
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
with open(HASHED_PASSWORD_FILE, 'wb') as f:
with open(hashed_password_file, 'wb') as f:
f.write(hashed)
os.chmod(HASHED_PASSWORD_FILE, 0o600)
os.chmod(hashed_password_file, 0o600)
logging.info("User password hashed and stored successfully (using alternative method).")
except Exception as e:
logging.error(f"Failed to store hashed password: {e}")
@@ -640,8 +938,8 @@ if __name__ == "__main__":
# Initialize PasswordManager
manager = PasswordManager()
# Initialize NostrClient with the parent seed from PasswordManager
nostr_client = NostrClient(parent_seed=manager.parent_seed)
# Initialize NostrClient with the EncryptionManager from PasswordManager
manager.nostr_client = NostrClient(encryption_manager=manager.encryption_manager)
# Example operations
# These would typically be triggered by user interactions, e.g., via a CLI menu
@@ -649,7 +947,6 @@ if __name__ == "__main__":
# manager.handle_retrieve_password()
# manager.handle_modify_entry()
# manager.handle_verify_checksum()
# manager.post_to_nostr(nostr_client)
# manager.retrieve_from_nostr(nostr_client)
# manager.nostr_client.publish_and_subscribe("Sample password data")
# manager.backup_database()
# manager.restore_database()

View File

@@ -12,64 +12,27 @@ Ensure that all dependencies are installed and properly configured in your envir
Never ever ever 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.
"""
import os
import logging
import hashlib
import base64
import string
import random
import traceback
from typing import Optional
from termcolor import colored
import random
from pathlib import Path
import shutil
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
from bip85.bip85 import BIP85
from local_bip85.bip85 import BIP85
from constants import DEFAULT_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH
from password_manager.encryption import EncryptionManager
# Configure logging at the start of the module
def configure_logging():
"""
Configures logging with both file and console handlers.
Logs include the timestamp, log level, message, filename, and line number.
Only ERROR and higher-level messages are shown in the terminal, while all messages
are logged in the log file.
"""
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG) # Set to DEBUG for detailed output
# Prevent adding multiple handlers if configure_logging is called multiple times
if not logger.handlers:
# 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_generation.log'))
# 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
formatter = logging.Formatter(
'%(asctime)s [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]'
)
c_handler.setFormatter(formatter)
f_handler.setFormatter(formatter)
# Add handlers to the logger
logger.addHandler(c_handler)
logger.addHandler(f_handler)
# Call the logging configuration function
configure_logging()
# Instantiate the logger
logger = logging.getLogger(__name__)
class PasswordGenerator:
@@ -81,24 +44,23 @@ class PasswordGenerator:
complexity requirements.
"""
def __init__(self, encryption_manager: EncryptionManager, parent_seed: str):
def __init__(self, encryption_manager: EncryptionManager, parent_seed: str, bip85: BIP85):
"""
Initializes the PasswordGenerator with the encryption manager and parent seed.
Initializes the PasswordGenerator with the encryption manager, parent seed, and BIP85 instance.
Parameters:
encryption_manager (EncryptionManager): The encryption manager instance.
parent_seed (str): The BIP-39 parent seed phrase.
bip85 (BIP85): The BIP85 instance for generating deterministic entropy.
"""
try:
self.encryption_manager = encryption_manager
self.parent_seed = parent_seed
self.bip85 = bip85
# Derive seed bytes from parent_seed using BIP39
# Derive seed bytes from parent_seed using BIP39 (handled by EncryptionManager)
self.seed_bytes = self.encryption_manager.derive_seed_from_mnemonic(self.parent_seed)
# Initialize BIP85 with seed_bytes
self.bip85 = BIP85(self.seed_bytes)
logger.debug("PasswordGenerator initialized successfully.")
except Exception as e:
logger.error(f"Failed to initialize PasswordGenerator: {e}")
@@ -112,7 +74,7 @@ class PasswordGenerator:
Steps:
1. Derive entropy using BIP-85.
2. Use PBKDF2-HMAC-SHA256 to derive a key from entropy.
2. Use HKDF-HMAC-SHA256 to derive a key from entropy.
3. Map the derived key to all allowed characters.
4. Ensure the password meets complexity requirements.
5. Shuffle the password deterministically based on the derived key.
@@ -126,6 +88,7 @@ class PasswordGenerator:
str: The generated password.
"""
try:
# Validate password length
if length < MIN_PASSWORD_LENGTH:
logger.error(f"Password length must be at least {MIN_PASSWORD_LENGTH} characters.")
raise ValueError(f"Password length must be at least {MIN_PASSWORD_LENGTH} characters.")
@@ -134,7 +97,7 @@ class PasswordGenerator:
raise ValueError(f"Password length must not exceed {MAX_PASSWORD_LENGTH} characters.")
# Derive entropy using BIP-85
entropy = self.bip85.derive_entropy(app_no=39, language_code=0, words_num=12, 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
@@ -167,17 +130,17 @@ class PasswordGenerator:
password_chars = list(password)
rng.shuffle(password_chars)
password = ''.join(password_chars)
logger.debug(f"Shuffled password deterministically.")
logger.debug("Shuffled password deterministically.")
# Ensure password length
# Ensure password length by extending if necessary
if len(password) < length:
# Extend the password deterministically
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)
logger.debug(f"Extended password: {password}")
# Trim the password to the desired length
password = password[:length]
logger.debug(f"Final password (trimmed to {length} chars): {password}")