diff --git a/src/password_manager/__init__.py b/src/password_manager/__init__.py index b081d8e..d74730f 100644 --- a/src/password_manager/__init__.py +++ b/src/password_manager/__init__.py @@ -5,9 +5,18 @@ 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'] +try: + from .config_manager import ConfigManager + + logging.info("ConfigManager module imported successfully.") +except Exception as e: + logging.error(f"Failed to import ConfigManager module: {e}") + logging.error(traceback.format_exc()) + +__all__ = ["PasswordManager", "ConfigManager"] diff --git a/src/password_manager/config_manager.py b/src/password_manager/config_manager.py new file mode 100644 index 0000000..d3c2f3d --- /dev/null +++ b/src/password_manager/config_manager.py @@ -0,0 +1,71 @@ +"""Config management for SeedPass profiles.""" + +from __future__ import annotations + +import logging +from pathlib import Path +from typing import List + +import bcrypt + +from password_manager.encryption import EncryptionManager +from nostr.client import DEFAULT_RELAYS as DEFAULT_NOSTR_RELAYS + +logger = logging.getLogger(__name__) + + +class ConfigManager: + """Manage per-profile configuration encrypted on disk.""" + + CONFIG_FILENAME = "seedpass_config.json.enc" + + def __init__(self, encryption_manager: EncryptionManager, fingerprint_dir: Path): + self.encryption_manager = encryption_manager + self.fingerprint_dir = fingerprint_dir + self.config_path = self.fingerprint_dir / self.CONFIG_FILENAME + + def load_config(self) -> dict: + """Load the configuration file, returning defaults if none exists.""" + if not self.config_path.exists(): + logger.info("Config file not found; returning defaults") + return {"relays": list(DEFAULT_NOSTR_RELAYS), "pin_hash": ""} + try: + data = self.encryption_manager.load_json_data(self.CONFIG_FILENAME) + if not isinstance(data, dict): + raise ValueError("Config data must be a dictionary") + # Ensure defaults for missing keys + data.setdefault("relays", list(DEFAULT_NOSTR_RELAYS)) + data.setdefault("pin_hash", "") + return data + except Exception as exc: + logger.error(f"Failed to load config: {exc}") + raise + + def save_config(self, config: dict) -> None: + """Encrypt and save configuration.""" + try: + self.encryption_manager.save_json_data(config, self.CONFIG_FILENAME) + except Exception as exc: + logger.error(f"Failed to save config: {exc}") + raise + + def set_relays(self, relays: List[str]) -> None: + """Update relay list and save.""" + config = self.load_config() + config["relays"] = relays + self.save_config(config) + + def set_pin(self, pin: str) -> None: + """Hash and store the provided PIN.""" + pin_hash = bcrypt.hashpw(pin.encode(), bcrypt.gensalt()).decode() + config = self.load_config() + config["pin_hash"] = pin_hash + self.save_config(config) + + def verify_pin(self, pin: str) -> bool: + """Check a provided PIN against the stored hash.""" + config = self.load_config() + stored = config.get("pin_hash", "").encode() + if not stored: + return False + return bcrypt.checkpw(pin.encode(), stored) diff --git a/src/tests/test_config_manager.py b/src/tests/test_config_manager.py new file mode 100644 index 0000000..9d13330 --- /dev/null +++ b/src/tests/test_config_manager.py @@ -0,0 +1,29 @@ +import bcrypt +from pathlib import Path +from tempfile import TemporaryDirectory +from cryptography.fernet import Fernet +import sys + +sys.path.append(str(Path(__file__).resolve().parents[1])) + +from password_manager.encryption import EncryptionManager +from password_manager.config_manager import ConfigManager +from nostr.client import DEFAULT_RELAYS + + +def test_config_defaults_and_round_trip(): + with TemporaryDirectory() as tmpdir: + key = Fernet.generate_key() + enc_mgr = EncryptionManager(key, Path(tmpdir)) + cfg_mgr = ConfigManager(enc_mgr, Path(tmpdir)) + + cfg = cfg_mgr.load_config() + assert cfg["relays"] == list(DEFAULT_RELAYS) + assert cfg["pin_hash"] == "" + + cfg_mgr.set_pin("1234") + cfg_mgr.set_relays(["wss://example.com"]) + + cfg2 = cfg_mgr.load_config() + assert cfg2["relays"] == ["wss://example.com"] + assert bcrypt.checkpw(b"1234", cfg2["pin_hash"].encode())