This commit is contained in:
Keep Creating Online
2024-10-19 19:55:59 -04:00
parent b79b44f036
commit c6a768131d
27 changed files with 4146 additions and 4 deletions

View File

@@ -0,0 +1,13 @@
# password_manager/__init__.py
import logging
import traceback
try:
from .manager import PasswordManager
logging.info("PasswordManager module imported successfully.")
except Exception as e:
logging.error(f"Failed to import PasswordManager module: {e}")
logging.error(traceback.format_exc()) # Log full traceback
__all__ = ['PasswordManager']

View File

@@ -0,0 +1,210 @@
# password_manager/backup.py
"""
Backup Manager Module
This module implements the BackupManager class, responsible for creating backups,
restoring from backups, and listing available backups for the encrypted password
index file. It ensures data integrity and provides mechanisms to recover from
corrupted or lost data by maintaining timestamped backups.
Dependencies:
- shutil
- time
- logging
- pathlib
- colorama
- termcolor
Ensure that all dependencies are installed and properly configured in your environment.
"""
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
# 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()
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
timestamped filenames to facilitate easy identification and retrieval.
"""
BACKUP_FILENAME_TEMPLATE = 'passwords_db_backup_{timestamp}.json.enc'
def __init__(self):
"""
Initializes the BackupManager with the application directory and index file paths.
"""
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}")
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}'.")
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
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}'.")
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
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.
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:
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'))
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
if not backup_file.exists():
logging.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}'.")
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

View File

@@ -0,0 +1,369 @@
# password_manager/encryption.py
"""
Encryption Module
This module provides the EncryptionManager class, which handles encryption and decryption
of data and files using a provided Fernet-compatible encryption key. This class ensures
that sensitive data is securely stored and retrieved, maintaining the confidentiality and integrity
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.
"""
import os
import json
import hashlib
import logging
import traceback
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()
logger = logging.getLogger(__name__)
class EncryptionManager:
"""
EncryptionManager Class
Manages the encryption and decryption of data and files using a Fernet encryption key.
"""
def __init__(self, encryption_key: bytes):
try:
self.fernet = Fernet(encryption_key)
logger.debug("EncryptionManager initialized with provided encryption key.")
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: str, file_path: Path) -> None:
"""
Encrypts and saves the parent seed to the specified file.
:param parent_seed: The BIP39 parent seed phrase.
:param file_path: The path to the file where the encrypted parent seed will be saved.
"""
try:
# **Do not encrypt the data here**
data = parent_seed.encode('utf-8')
# Pass the raw data to encrypt_file, which handles encryption
self.encrypt_file(file_path, data)
logger.info(f"Parent seed encrypted and saved to '{file_path}'.")
print(colored(f"Parent seed encrypted and saved to '{file_path}'.", 'green'))
except Exception as e:
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:
"""
Encrypts the provided data and writes it to the specified file with file locking.
:param file_path: The path to the file where encrypted data will be written.
:param data: The plaintext data to encrypt and write.
"""
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'))
except Exception as e:
logger.error(f"Failed to encrypt and write to file '{file_path}': {e}")
logger.error(traceback.format_exc())
print(colored(f"Error: Failed to encrypt and write to file '{file_path}': {e}", 'red'))
raise
def encrypt_data(self, data: bytes) -> bytes:
"""
Encrypts the given plaintext data.
:param data: The plaintext data to encrypt.
:return: The encrypted data as bytes.
"""
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(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to encrypt data: {e}", 'red'))
raise
def decrypt_data(self, encrypted_data: bytes) -> bytes:
"""
Decrypts the given encrypted data.
:param encrypted_data: The encrypted data to decrypt.
:return: The decrypted plaintext 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.")
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
print(colored(f"Error: Failed to decrypt data: {e}", 'red'))
raise
def decrypt_file(self, file_path: Path) -> bytes:
"""
Decrypts the data from the specified file.
:param file_path: The path to the file containing encrypted data.
:return: The decrypted plaintext data as bytes.
"""
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
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'))
raise
def save_json_data(self, data: dict, file_path: Optional[Path] = None) -> None:
"""
Encrypts and saves the provided JSON data to the specified file.
:param data: The JSON data to save.
:param file_path: The path to the file where data will be saved. Defaults to INDEX_FILE.
"""
if file_path is None:
file_path = INDEX_FILE
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'))
except Exception as e:
logger.error(f"Failed to save JSON data to '{file_path}': {e}")
logger.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to save JSON data to '{file_path}': {e}", 'red'))
raise
def load_json_data(self, file_path: Optional[Path] = None) -> dict:
"""
Decrypts and loads JSON data from the specified file.
:param file_path: The path to the file from which data will be loaded. Defaults to INDEX_FILE.
:return: The decrypted JSON data as a dictionary.
"""
if file_path is None:
file_path = INDEX_FILE
if not file_path.exists():
logger.info(f"Index file '{file_path}' does not exist. Initializing empty data.")
print(colored(f"Info: Index file '{file_path}' not found. Initializing new password database.", 'yellow'))
return {'passwords': {}}
try:
decrypted_data = self.decrypt_file(file_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}")
print(colored(f"JSON data loaded and decrypted from '{file_path}'.", 'green'))
return data
except json.JSONDecodeError as e:
logger.error(f"Failed to decode JSON data from '{file_path}': {e}")
logger.error(traceback.format_exc())
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.")
print(colored("Error: Invalid encryption key or corrupted data.", 'red'))
raise
except Exception as e:
logger.error(f"Failed to load JSON data from '{file_path}': {e}")
logger.error(traceback.format_exc())
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:
"""
Updates the checksum file for the specified file.
:param file_path: The path to the file for which the checksum will be updated.
Defaults to INDEX_FILE.
"""
if file_path is None:
file_path = INDEX_FILE
try:
decrypted_data = self.decrypt_file(file_path)
content = decrypted_data.decode('utf-8')
checksum = hashlib.sha256(content.encode('utf-8')).hexdigest()
checksum_file = file_path.parent / f"{file_path.stem}_checksum.txt"
with open(checksum_file, 'w') as f:
f.write(checksum)
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(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to update checksum for '{file_path}': {e}", 'red'))
raise
def get_encrypted_index(self) -> Optional[bytes]:
"""
Retrieves the encrypted password index file content.
: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:
encrypted_data = file.read()
logger.debug(f"Encrypted index data read from '{INDEX_FILE}'.")
return encrypted_data
except Exception as e:
logger.error(f"Failed to read encrypted index file '{INDEX_FILE}': {e}")
logger.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to read encrypted index file '{INDEX_FILE}': {e}", 'red'))
return None
def decrypt_and_save_index_from_nostr(self, encrypted_data: bytes) -> None:
"""
Decrypts the encrypted data retrieved from Nostr and updates the local index file.
:param encrypted_data: The encrypted data retrieved from Nostr.
"""
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)
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("Parent seed decrypted successfully.")
return parent_seed
except Exception as e:
logger.error(f"Failed to decrypt parent seed from '{file_path}': {e}")
logger.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to decrypt parent seed from '{file_path}': {e}", 'red'))
raise
def validate_seed(self, seed_phrase: str) -> bool:
"""
Validates the seed phrase format using BIP-39 standards.
:param seed_phrase: The BIP39 seed phrase to validate.
: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
except Exception as e:
logger.error(f"Error validating seed phrase: {e}")
logger.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to validate seed phrase: {e}", 'red'))
return False
def derive_seed_from_mnemonic(self, mnemonic: str, passphrase: str = "") -> bytes:
"""
Derives a cryptographic seed from a BIP39 mnemonic (seed phrase).
:param mnemonic: The BIP39 mnemonic phrase.
:param passphrase: An optional passphrase for additional security.
:return: The derived seed as bytes.
"""
try:
mnemo = Mnemonic("english")
seed = mnemo.to_seed(mnemonic, 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()) # Log full traceback
print(colored(f"Error: Failed to derive seed from mnemonic: {e}", 'red'))
raise

View File

@@ -0,0 +1,458 @@
# password_manager/entry_management.py
"""
Entry Management Module
This module implements the EntryManager class, responsible for handling
operations related to managing password entries in the deterministic password manager.
It provides methods to add, retrieve, modify, and list password entries, ensuring
that all changes are securely encrypted and properly indexed.
Dependencies:
- password_manager.encryption.EncryptionManager
- constants.INDEX_FILE
- constants.DATA_CHECKSUM_FILE
- utils.file_lock.lock_file
- colorama.Fore
- termcolor.colored
- logging
- json
- hashlib
- sys
- os
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.
"""
import json
import logging
import hashlib
import sys
import os
import shutil
import time
import traceback
from typing import Optional, Tuple, Dict, Any, List
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()
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):
"""
Initializes the EntryManager with an instance of EncryptionManager.
:param encryption_manager: An instance of EncryptionManager for handling encryption.
"""
try:
self.encryption_manager = encryption_manager
logger.debug("EntryManager initialized with provided EncryptionManager.")
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)
def get_next_index(self) -> int:
"""
Retrieves the next available index for a new password entry.
:return: The next index number as an integer.
"""
try:
data = self.encryption_manager.load_json_data()
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
else:
next_index = 0
logger.debug(f"Next index determined: {next_index}")
return next_index
except Exception as e:
logger.error(f"Error determining next index: {e}")
logger.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error determining next index: {e}", 'red'))
sys.exit(1)
def add_entry(self, website_name: str, length: int, username: Optional[str] = None,
url: Optional[str] = None, blacklisted: bool = False) -> int:
"""
Adds a new password entry to the encrypted JSON index file.
:param website_name: The name of the website.
:param length: The desired length of the password.
:param username: (Optional) The username associated with the website.
:param url: (Optional) The URL of the website.
:param blacklisted: (Optional) Whether the password is blacklisted. Defaults to False.
:return: The assigned index of the new entry.
"""
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['passwords'][str(index)] = {
'website': website_name,
'length': length,
'username': username if username else '',
'url': url if url else '',
'blacklisted': blacklisted
}
logger.debug(f"Added entry at index {index}: {data['passwords'][str(index)]}")
self.encryption_manager.save_json_data(data)
self.update_checksum()
self.backup_index_file()
logger.info(f"Entry added successfully at index {index}.")
print(colored(f"[+] Entry added successfully at index {index}.", 'green'))
return index # Return the assigned index
except Exception as e:
logger.error(f"Failed to add entry: {e}")
logger.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to add entry: {e}", 'red'))
sys.exit(1)
def retrieve_entry(self, index: int) -> Optional[Dict[str, Any]]:
"""
Retrieves a password entry based on the provided index.
:param index: The index number of the password entry.
:return: A dictionary containing the entry details or None if not found.
"""
try:
data = self.encryption_manager.load_json_data()
entry = data.get('passwords', {}).get(str(index))
if entry:
logger.debug(f"Retrieved entry at index {index}: {entry}")
return entry
else:
logger.warning(f"No entry found at index {index}.")
print(colored(f"Warning: No entry found at index {index}.", 'yellow'))
return None
except Exception as e:
logger.error(f"Failed to retrieve entry at index {index}: {e}")
logger.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to retrieve entry at index {index}: {e}", 'red'))
return None
def modify_entry(self, index: int, username: Optional[str] = None,
url: Optional[str] = None,
blacklisted: Optional[bool] = None) -> None:
"""
Modifies an existing password entry based on the provided index and new values.
:param index: The index number of the password entry to modify.
:param username: (Optional) The new username.
:param url: (Optional) The new URL.
:param blacklisted: (Optional) The new blacklist status.
"""
try:
data = self.encryption_manager.load_json_data()
entry = data.get('passwords', {}).get(str(index))
if not entry:
logger.warning(f"No entry found at index {index}. Cannot modify non-existent entry.")
print(colored(f"Warning: No entry found at index {index}. Cannot modify non-existent entry.", 'yellow'))
return
if username is not None:
entry['username'] = username
logger.debug(f"Updated username to '{username}' for index {index}.")
if url is not None:
entry['url'] = url
logger.debug(f"Updated URL to '{url}' for index {index}.")
if blacklisted is not None:
entry['blacklisted'] = blacklisted
logger.debug(f"Updated blacklist status to '{blacklisted}' for index {index}.")
data['passwords'][str(index)] = entry
logger.debug(f"Modified entry at index {index}: {entry}")
self.encryption_manager.save_json_data(data)
self.update_checksum()
self.backup_index_file()
logger.info(f"Entry at index {index} modified successfully.")
print(colored(f"[+] Entry at index {index} modified successfully.", 'green'))
except Exception as e:
logger.error(f"Failed to modify entry at index {index}: {e}")
logger.error(traceback.format_exc()) # Log full traceback
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]]:
"""
Lists all password entries in the index.
:return: A list of tuples containing entry details: (index, website, username, url, blacklisted)
"""
try:
data = self.encryption_manager.load_json_data()
passwords = data.get('passwords', {})
if not passwords:
logger.info("No password entries found.")
print(colored("No password entries found.", 'yellow'))
return []
entries = []
for idx, entry in sorted(passwords.items(), key=lambda x: int(x[0])):
entries.append((
int(idx),
entry.get('website', ''),
entry.get('username', ''),
entry.get('url', ''),
entry.get('blacklisted', False)
))
logger.debug(f"Total entries found: {len(entries)}")
for entry in entries:
print(colored(f"Index: {entry[0]}", 'cyan'))
print(colored(f" Website: {entry[1]}", 'cyan'))
print(colored(f" Username: {entry[2] or 'N/A'}", 'cyan'))
print(colored(f" URL: {entry[3] or 'N/A'}", 'cyan'))
print(colored(f" Blacklisted: {'Yes' if entry[4] else 'No'}", 'cyan'))
print("-" * 40)
return entries
except Exception as e:
logger.error(f"Failed to list entries: {e}")
logger.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to list entries: {e}", 'red'))
return []
def delete_entry(self, index: int) -> None:
"""
Deletes a password entry based on the provided index.
:param index: The index number of the password entry to delete.
"""
try:
data = self.encryption_manager.load_json_data()
if 'passwords' in data and str(index) in data['passwords']:
del data['passwords'][str(index)]
logger.debug(f"Deleted entry at index {index}.")
self.encryption_manager.save_json_data(data)
self.update_checksum()
self.backup_index_file()
logger.info(f"Entry at index {index} deleted successfully.")
print(colored(f"[+] Entry at index {index} deleted successfully.", 'green'))
else:
logger.warning(f"No entry found at index {index}. Cannot delete non-existent entry.")
print(colored(f"Warning: No entry found at index {index}. Cannot delete non-existent entry.", 'yellow'))
except Exception as e:
logger.error(f"Failed to delete entry at index {index}: {e}")
logger.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to delete entry at index {index}: {e}", 'red'))
def update_checksum(self) -> None:
"""
Updates the checksum file for the password database to ensure data integrity.
"""
try:
data = self.encryption_manager.load_json_data()
json_content = json.dumps(data, indent=4)
checksum = hashlib.sha256(json_content.encode('utf-8')).hexdigest()
with open(DATA_CHECKSUM_FILE, 'w') as f:
f.write(checksum)
logger.debug(f"Checksum updated and written to '{DATA_CHECKSUM_FILE}'.")
print(colored(f"[+] Checksum updated successfully.", 'green'))
except Exception as e:
logger.error(f"Failed to update checksum: {e}")
logger.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to update checksum: {e}", 'red'))
def backup_index_file(self) -> None:
"""
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.")
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)
with open(INDEX_FILE, '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}'.")
print(colored(f"[+] Backup created at '{backup_path}'.", 'green'))
except Exception as e:
logger.error(f"Failed to create backup: {e}")
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.
:param backup_path: The file path of the backup to restore from.
"""
try:
if not os.path.exists(backup_path):
logger.error(f"Backup file '{backup_path}' does not exist.")
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:
shutil.copyfileobj(backup_file, index_file)
logger.debug(f"Index file restored from backup '{backup_path}'.")
print(colored(f"[+] Index file restored from backup '{backup_path}'.", 'green'))
self.update_checksum()
except Exception as e:
logger.error(f"Failed to restore from backup '{backup_path}': {e}")
logger.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to restore from backup '{backup_path}': {e}", 'red'))
def list_all_entries(self) -> None:
"""
Displays all password entries in a formatted manner.
"""
try:
entries = self.list_entries()
if not entries:
print(colored("No entries to display.", 'yellow'))
return
print(colored("\n[+] Listing All Password Entries:\n", 'green'))
for entry in entries:
index, website, username, url, blacklisted = entry
print(colored(f"Index: {index}", 'cyan'))
print(colored(f" Website: {website}", 'cyan'))
print(colored(f" Username: {username or 'N/A'}", 'cyan'))
print(colored(f" URL: {url or 'N/A'}", 'cyan'))
print(colored(f" Blacklisted: {'Yes' if blacklisted else 'No'}", 'cyan'))
print("-" * 40)
except Exception as e:
logger.error(f"Failed to list all entries: {e}")
logger.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to list all entries: {e}", 'red'))
return
# Example usage (this part should be removed or commented out when integrating into the larger application)
if __name__ == "__main__":
from password_manager.encryption import EncryptionManager # Ensure this import is correct based on your project structure
# Initialize EncryptionManager with a dummy key for demonstration purposes
# Replace 'your-fernet-key' with your actual Fernet key
try:
dummy_key = Fernet.generate_key()
encryption_manager = EncryptionManager(dummy_key)
except Exception as e:
logger.error(f"Failed to initialize EncryptionManager: {e}")
print(colored(f"Error: Failed to initialize EncryptionManager: {e}", 'red'))
sys.exit(1)
# Initialize EntryManager
try:
entry_manager = EntryManager(encryption_manager)
except Exception as e:
logger.error(f"Failed to initialize EntryManager: {e}")
print(colored(f"Error: Failed to initialize EntryManager: {e}", 'red'))
sys.exit(1)
# Example operations
# These would typically be triggered by user interactions, e.g., via a CLI menu
# Uncomment and modify the following lines as needed for testing
# Adding an entry
# entry_manager.add_entry("Example Website", 16, "user123", "https://example.com", False)
# Listing all entries
# entry_manager.list_all_entries()
# Retrieving an entry
# entry = entry_manager.retrieve_entry(0)
# if entry:
# print(entry)
# Modifying an entry
# entry_manager.modify_entry(0, username="new_user123")
# Deleting an entry
# entry_manager.delete_entry(0)
# Restoring from a backup
# entry_manager.restore_from_backup("path_to_backup_file.json.enc")

View File

@@ -0,0 +1,506 @@
# password_manager/manager.py
"""
Password Manager Module
This module implements the PasswordManager class, which orchestrates various functionalities
of the deterministic password manager, including encryption, entry management, password
generation, backup, and checksum verification. It serves as the core interface for interacting
with the password manager functionalities.
Dependencies:
- password_manager.encryption.EncryptionManager
- password_manager.entry_management.EntryManager
- password_manager.password_generation.PasswordGenerator
- password_manager.backup.BackupManager
- utils.key_derivation.derive_key_from_parent_seed
- utils.key_derivation.derive_key_from_password
- utils.checksum.calculate_checksum
- utils.checksum.verify_checksum
- utils.password_prompt.prompt_for_password
- constants.APP_DIR
- constants.INDEX_FILE
- constants.PARENT_SEED_FILE
- constants.DATA_CHECKSUM_FILE
- constants.SCRIPT_CHECKSUM_FILE
- constants.MIN_PASSWORD_LENGTH
- constants.MAX_PASSWORD_LENGTH
- constants.DEFAULT_PASSWORD_LENGTH
- colorama.Fore
- termcolor.colored
- logging
- json
- sys
- os
- getpass
Ensure that all dependencies are installed and properly configured in your environment.
"""
# password_manager/manager.py
"""
Password Manager Module
This module implements the PasswordManager class, which orchestrates various functionalities
of the deterministic password manager, including encryption, entry management, password
generation, backup, and checksum verification. It serves as the core interface for interacting
with the password manager functionalities.
"""
import sys
import json
import logging
import getpass
import os
from typing import Optional
from colorama import Fore
from termcolor import colored
from password_manager.encryption import EncryptionManager
from password_manager.entry_management import EntryManager
from password_manager.password_generation import PasswordGenerator
from password_manager.backup import BackupManager
from utils.key_derivation import derive_key_from_parent_seed, derive_key_from_password
from utils.checksum import calculate_checksum, verify_checksum
from utils.password_prompt import prompt_for_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
)
import traceback # Added for exception traceback logging
# 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_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()
class PasswordManager:
"""
PasswordManager Class
Manages the generation, encryption, and retrieval of deterministic passwords using a BIP-39 seed.
It handles file encryption/decryption, password generation, entry management, backups, and checksum
verification, ensuring the integrity and confidentiality of the stored password database.
"""
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.
"""
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 # Added parent_seed attribute
self.setup_parent_seed()
self.initialize_managers()
def setup_parent_seed(self) -> None:
if os.path.exists(PARENT_SEED_FILE):
# Parent seed file exists, prompt for password to decrypt
password = getpass.getpass(prompt='Enter your login password: ').strip()
try:
# 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)
# **Add validation for the decrypted seed**
if not self.validate_seed_phrase(self.parent_seed):
logging.error("Decrypted seed is invalid. Exiting.")
print(colored("Error: Decrypted seed is invalid.", 'red'))
sys.exit(1)
logging.debug("Parent seed decrypted and validated successfully.")
except Exception as e:
logging.error(f"Failed to decrypt parent seed: {e}")
logging.error(traceback.format_exc())
print(colored(f"Error: Failed to decrypt parent seed: {e}", 'red'))
sys.exit(1)
else:
# First-time setup: prompt for parent seed and password
try:
parent_seed = getpass.getpass(prompt='Enter your 12-word parent seed: ').strip()
# Validate parent seed (basic validation)
parent_seed = self.basic_validate_seed_phrase(parent_seed)
if not parent_seed:
logging.error("Invalid seed phrase. Exiting.")
sys.exit(1)
except KeyboardInterrupt:
logging.info("Operation cancelled by user.")
print(colored("\nOperation cancelled by user.", 'yellow'))
sys.exit(0)
# Prompt for password
password = prompt_for_password()
# Derive encryption key from password
key = derive_key_from_password(password)
self.encryption_manager = EncryptionManager(key)
# Encrypt and save the parent seed
try:
self.encryption_manager.encrypt_parent_seed(parent_seed, PARENT_SEED_FILE)
logging.info("Parent seed encrypted and saved successfully.")
except Exception as e:
logging.error(f"Failed to encrypt and save parent seed: {e}")
logging.error(traceback.format_exc())
print(colored(f"Error: Failed to encrypt and save parent seed: {e}", 'red'))
sys.exit(1)
self.parent_seed = parent_seed
def basic_validate_seed_phrase(self, seed_phrase: str) -> Optional[str]:
"""
Performs basic validation on the seed phrase without relying on EncryptionManager.
Parameters:
seed_phrase (str): The seed phrase to validate.
Returns:
Optional[str]: The validated seed phrase or None if invalid.
"""
try:
words = seed_phrase.split()
if len(words) != 12:
logging.error("Seed phrase must contain exactly 12 words.")
print(colored("Error: Seed phrase must contain exactly 12 words.", 'red'))
return None
# Additional basic validations can be added here (e.g., word list checks)
logging.debug("Seed phrase validated successfully.")
return seed_phrase
except Exception as e:
logging.error(f"Error during basic seed validation: {e}")
logging.error(traceback.format_exc())
print(colored(f"Error: {e}", 'red'))
return None
def validate_seed_phrase(self, seed_phrase: str) -> Optional[str]:
"""
Validates the seed phrase using the EncryptionManager if available,
otherwise performs basic validation.
Parameters:
seed_phrase (str): The seed phrase to validate.
Returns:
Optional[str]: The validated seed phrase or None if invalid.
"""
try:
if self.encryption_manager:
# Use EncryptionManager to validate seed
if self.encryption_manager.validate_seed(seed_phrase):
logging.debug("Seed phrase validated successfully using EncryptionManager.")
return seed_phrase
else:
logging.error("Invalid seed phrase.")
print(colored("Error: Invalid seed phrase.", 'red'))
return None
else:
# Perform basic validation
return self.basic_validate_seed_phrase(seed_phrase)
except Exception as e:
logging.error(f"Error validating seed phrase: {e}")
logging.error(traceback.format_exc())
print(colored(f"Error: Failed to validate seed phrase: {e}", 'red'))
return None
def initialize_managers(self) -> None:
"""
Initializes the EntryManager, PasswordGenerator, and BackupManager with the EncryptionManager
and parent seed.
"""
try:
self.entry_manager = EntryManager(self.encryption_manager)
self.password_generator = PasswordGenerator(self.encryption_manager, self.parent_seed)
self.backup_manager = BackupManager()
logging.debug("EntryManager, PasswordGenerator, and BackupManager initialized.")
except Exception as e:
logging.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)
def handle_generate_password(self) -> None:
try:
website_name = input('Enter the website name: ').strip()
if not website_name:
print(colored("Error: Website name cannot be empty.", 'red'))
return
username = input('Enter the username (optional): ').strip()
url = input('Enter the URL (optional): ').strip()
length_input = input(f'Enter desired password length (default {DEFAULT_PASSWORD_LENGTH}): ').strip()
length = DEFAULT_PASSWORD_LENGTH
if length_input:
if not length_input.isdigit():
print(colored("Error: Password length must be a number.", 'red'))
return
length = int(length_input)
if not (MIN_PASSWORD_LENGTH <= length <= MAX_PASSWORD_LENGTH):
print(colored(f"Error: Password length must be between {MIN_PASSWORD_LENGTH} and {MAX_PASSWORD_LENGTH}.", 'red'))
return
# Add the entry to the index and get the assigned index
index = self.entry_manager.add_entry(website_name, length, username, url, blacklisted=False)
# Generate the password using the assigned index
password = self.password_generator.generate_password(length, index)
# Provide user feedback
print(colored(f"\n[+] Password generated and indexed with ID {index}.\n", 'green'))
print(colored(f"Password for {website_name}: {password}\n", 'yellow'))
except Exception as e:
logging.error(f"Error during password generation: {e}")
logging.error(traceback.format_exc())
print(colored(f"Error: Failed to generate password: {e}", 'red'))
def handle_retrieve_password(self) -> None:
"""
Handles retrieving a password from the index by prompting the user for the index number
and displaying the corresponding password and associated details.
"""
try:
index_input = input('Enter the index number of the password to retrieve: ').strip()
if not index_input.isdigit():
print(colored("Error: Index must be a number.", 'red'))
return
index = int(index_input)
# Retrieve entry details
entry = self.entry_manager.retrieve_entry(index)
if not entry:
return
# Display entry details
website_name = entry.get('website')
length = entry.get('length')
username = entry.get('username')
url = entry.get('url')
blacklisted = entry.get('blacklisted')
print(colored(f"Retrieving password for '{website_name}' with length {length}.", 'cyan'))
if username:
print(colored(f"Username: {username}", 'cyan'))
if url:
print(colored(f"URL: {url}", 'cyan'))
if blacklisted:
print(colored(f"Warning: This password is blacklisted and should not be used.", 'red'))
# Generate the password
password = self.password_generator.generate_password(length, index)
# Display the password and associated details
if password:
print(colored(f"\n[+] Retrieved Password for {website_name}:\n", 'green'))
print(colored(f"Password: {password}", 'yellow'))
print(colored(f"Associated Username: {username or 'N/A'}", 'cyan'))
print(colored(f"Associated URL: {url or 'N/A'}", 'cyan'))
print(colored(f"Blacklist Status: {'Blacklisted' if blacklisted else 'Not Blacklisted'}", 'cyan'))
else:
print(colored("Error: Failed to retrieve the password.", 'red'))
except Exception as e:
logging.error(f"Error during password retrieval: {e}")
logging.error(traceback.format_exc())
print(colored(f"Error: Failed to retrieve password: {e}", 'red'))
def handle_modify_entry(self) -> None:
"""
Handles modifying an existing password entry by prompting the user for the index number
and new details to update.
"""
try:
index_input = input('Enter the index number of the entry to modify: ').strip()
if not index_input.isdigit():
print(colored("Error: Index must be a number.", 'red'))
return
index = int(index_input)
# Retrieve existing entry
entry = self.entry_manager.retrieve_entry(index)
if not entry:
return
website_name = entry.get('website')
length = entry.get('length')
username = entry.get('username')
url = entry.get('url')
blacklisted = entry.get('blacklisted')
# Display current values
print(colored(f"Modifying entry for '{website_name}' (Index: {index}):", 'cyan'))
print(colored(f"Current Username: {username or 'N/A'}", 'cyan'))
print(colored(f"Current URL: {url or 'N/A'}", 'cyan'))
print(colored(f"Current Blacklist Status: {'Blacklisted' if blacklisted else 'Not Blacklisted'}", 'cyan'))
# Prompt for new values (optional)
new_username = input(f'Enter new username (leave blank to keep "{username or "N/A"}"): ').strip() or username
new_url = input(f'Enter new URL (leave blank to keep "{url or "N/A"}"): ').strip() or url
blacklist_input = input(f'Is this password blacklisted? (Y/N, current: {"Y" if blacklisted else "N"}): ').strip().lower()
if blacklist_input == '':
new_blacklisted = blacklisted
elif blacklist_input == 'y':
new_blacklisted = True
elif blacklist_input == 'n':
new_blacklisted = False
else:
print(colored("Invalid input for blacklist status. Keeping the current status.", 'yellow'))
new_blacklisted = blacklisted
# Update the entry
self.entry_manager.modify_entry(index, new_username, new_url, new_blacklisted)
print(colored(f"Entry updated successfully for index {index}.", 'green'))
except Exception as e:
logging.error(f"Error during modifying entry: {e}")
logging.error(traceback.format_exc())
print(colored(f"Error: Failed to modify entry: {e}", 'red'))
def handle_verify_checksum(self) -> None:
"""
Handles verifying the script's checksum against the stored checksum to ensure integrity.
"""
try:
current_checksum = calculate_checksum(__file__)
if verify_checksum(current_checksum, SCRIPT_CHECKSUM_FILE):
print(colored("Checksum verification passed.", 'green'))
logging.info("Checksum verification passed.")
else:
print(colored("Checksum verification failed. The script may have been modified.", 'red'))
logging.error("Checksum verification failed.")
except Exception as e:
logging.error(f"Error during checksum verification: {e}")
logging.error(traceback.format_exc())
print(colored(f"Error: Failed to verify checksum: {e}", 'red'))
def get_encrypted_data(self) -> Optional[bytes]:
"""
Retrieves the encrypted password index data.
:return: The encrypted data as bytes, or None if retrieval fails.
"""
try:
encrypted_data = self.encryption_manager.get_encrypted_index()
if encrypted_data:
logging.debug("Encrypted index data retrieved successfully.")
return encrypted_data
else:
logging.error("Failed to retrieve encrypted index data.")
print(colored("Error: Failed to retrieve encrypted index data.", 'red'))
return None
except Exception as e:
logging.error(f"Error retrieving encrypted data: {e}")
logging.error(traceback.format_exc())
print(colored(f"Error: Failed to retrieve encrypted data: {e}", 'red'))
return None
def decrypt_and_save_index_from_nostr(self, encrypted_data: bytes) -> None:
"""
Decrypts the encrypted data retrieved from Nostr and updates the local index.
:param encrypted_data: The encrypted data retrieved from Nostr.
"""
try:
self.encryption_manager.decrypt_and_save_index_from_nostr(encrypted_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'))
def backup_database(self) -> None:
"""
Creates a backup of the encrypted JSON index file.
"""
try:
self.backup_manager.create_backup()
print(colored("Backup created successfully.", 'green'))
except Exception as e:
logging.error(f"Failed to create backup: {e}")
logging.error(traceback.format_exc())
print(colored(f"Error: Failed to create backup: {e}", 'red'))
def restore_database(self) -> None:
"""
Restores the encrypted JSON index file from the latest backup.
"""
try:
self.backup_manager.restore_latest_backup()
print(colored("Database restored from the latest backup successfully.", 'green'))
except Exception as e:
logging.error(f"Failed to restore backup: {e}")
logging.error(traceback.format_exc())
print(colored(f"Error: Failed to restore backup: {e}", 'red'))
# Additional methods can be added here as needed
# Example usage (this part should be removed or commented out when integrating into the larger application)
if __name__ == "__main__":
from nostr.client import NostrClient # Ensure this import is correct based on your project structure
# Initialize PasswordManager
manager = PasswordManager()
# Initialize NostrClient with the parent seed from PasswordManager
nostr_client = NostrClient(parent_seed=manager.parent_seed)
# Example operations
# These would typically be triggered by user interactions, e.g., via a CLI menu
# manager.handle_generate_password()
# 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.backup_database()
# manager.restore_database()

View File

@@ -0,0 +1,264 @@
# password_manager/password_generation.py
"""
Password Generation Module
This module provides the PasswordGenerator class responsible for deterministic password generation
based on a BIP-39 parent seed. It leverages BIP-85 for entropy derivation and ensures that
generated passwords meet complexity requirements.
Dependencies:
- bip85.BIP85
- cryptography.hazmat.primitives.hashes
- cryptography.hazmat.primitives.kdf.hkdf
- cryptography.hazmat.backends.default_backend
- constants.py
- password_manager.encryption.EncryptionManager
- logging
- hashlib
- hmac
- base64
- string
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.
"""
import os
import logging
import hashlib
import hmac
import base64
import string
import traceback
from typing import Optional
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 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()
logger = logging.getLogger(__name__)
class PasswordGenerator:
"""
PasswordGenerator Class
Responsible for deterministic password generation based on a BIP-39 parent seed.
Utilizes BIP-85 for entropy derivation and ensures that generated passwords meet
complexity requirements.
"""
def __init__(self, encryption_manager: EncryptionManager, parent_seed: str):
"""
Initializes the PasswordGenerator with the encryption manager and parent seed.
Parameters:
encryption_manager (EncryptionManager): The encryption manager instance.
parent_seed (str): The BIP-39 parent seed phrase.
"""
try:
self.encryption_manager = encryption_manager
self.parent_seed = parent_seed
# Derive seed bytes from parent_seed using BIP39
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}")
logger.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to initialize PasswordGenerator: {e}", 'red'))
raise
def generate_password(self, length: int = DEFAULT_PASSWORD_LENGTH, index: int = 0) -> str:
"""
Generates a deterministic password based on the parent seed, desired length, and index.
Steps:
1. Derive entropy using BIP-85.
2. Use PBKDF2-HMAC-SHA256 to derive a key from entropy.
3. Base64-encode the derived key and filter to allowed characters.
4. Ensure the password meets complexity requirements.
Parameters:
length (int): Desired length of the password.
index (int): Index for deriving child entropy.
Returns:
str: The generated password.
"""
try:
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.")
if length > MAX_PASSWORD_LENGTH:
logger.error(f"Password length must not exceed {MAX_PASSWORD_LENGTH} characters.")
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)
logger.debug(f"Derived entropy: {entropy.hex()}")
# Use HKDF to derive key from entropy
hkdf = HKDF(
algorithm=hashes.SHA256(),
length=32, # 256 bits for AES-256
salt=None,
info=b'password-generation',
backend=default_backend()
)
derived_key = hkdf.derive(entropy)
logger.debug(f"Derived key using HKDF: {derived_key.hex()}")
# Use PBKDF2-HMAC-SHA256 to derive a key from entropy
dk = hashlib.pbkdf2_hmac('sha256', entropy, b'', 100000)
logger.debug(f"Derived key using PBKDF2: {dk.hex()}")
# Base64 encode the derived key
base64_password = base64.b64encode(dk).decode('utf-8')
logger.debug(f"Base64 encoded password: {base64_password}")
# Filter to allowed characters
alphabet = string.ascii_letters + string.digits + string.punctuation
password = ''.join(filter(lambda x: x in alphabet, base64_password))
logger.debug(f"Password after filtering: {password}")
# Ensure the password meets complexity requirements
password = self.ensure_complexity(password, alphabet, dk)
logger.debug(f"Password after ensuring complexity: {password}")
# Ensure password length
if len(password) < length:
# Extend the password deterministically
while len(password) < length:
dk = hashlib.pbkdf2_hmac('sha256', dk, b'', 1)
base64_extra = base64.b64encode(dk).decode('utf-8')
password += ''.join(filter(lambda x: x in alphabet, base64_extra))
logger.debug(f"Extended password: {password}")
password = password[:length]
logger.debug(f"Final password (trimmed to {length} chars): {password}")
return password
except Exception as e:
logger.error(f"Error generating password: {e}")
logger.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to generate password: {e}", 'red'))
raise
def ensure_complexity(self, password: str, alphabet: str, dk: bytes) -> str:
"""
Ensures that the password contains at least one uppercase letter, one lowercase letter,
one digit, and one special character, modifying it deterministically if necessary.
Parameters:
password (str): The initial password.
alphabet (str): Allowed characters in the password.
dk (bytes): Derived key used for deterministic modifications.
Returns:
str: Password that meets complexity requirements.
"""
try:
uppercase = string.ascii_uppercase
lowercase = string.ascii_lowercase
digits = string.digits
special = string.punctuation
password_chars = list(password)
has_upper = any(c in uppercase for c in password_chars)
has_lower = any(c in lowercase for c in password_chars)
has_digit = any(c in digits for c in password_chars)
has_special = any(c in special for c in password_chars)
dk_index = 0
dk_length = len(dk)
def get_dk_value() -> int:
nonlocal dk_index
value = dk[dk_index % dk_length]
dk_index += 1
return value
if not has_upper:
index = get_dk_value() % len(password_chars)
char = uppercase[get_dk_value() % len(uppercase)]
password_chars[index] = char
logger.debug(f"Added uppercase letter '{char}' at position {index}.")
if not has_lower:
index = get_dk_value() % len(password_chars)
char = lowercase[get_dk_value() % len(lowercase)]
password_chars[index] = char
logger.debug(f"Added lowercase letter '{char}' at position {index}.")
if not has_digit:
index = get_dk_value() % len(password_chars)
char = digits[get_dk_value() % len(digits)]
password_chars[index] = char
logger.debug(f"Added digit '{char}' at position {index}.")
if not has_special:
index = get_dk_value() % len(password_chars)
char = special[get_dk_value() % len(special)]
password_chars[index] = char
logger.debug(f"Added special character '{char}' at position {index}.")
return ''.join(password_chars)
except Exception as e:
logger.error(f"Error ensuring password complexity: {e}")
logger.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to ensure password complexity: {e}", 'red'))
raise