"""Config management for SeedPass profiles.""" from __future__ import annotations import logging from pathlib import Path from typing import List, Optional import getpass 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, require_pin: bool = True) -> dict: """Load the configuration file and optionally verify a stored PIN. Parameters ---------- require_pin: bool, default True If True and a PIN is configured, prompt the user to enter it and verify against the stored hash. """ 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", "") if require_pin and data.get("pin_hash"): for _ in range(3): pin = getpass.getpass("Enter settings PIN: ").strip() if bcrypt.checkpw(pin.encode(), data["pin_hash"].encode()): break print("Invalid PIN") else: raise ValueError("PIN verification failed") 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], require_pin: bool = True) -> None: """Update relay list and save.""" if not relays: raise ValueError("At least one Nostr relay must be configured") config = self.load_config(require_pin=require_pin) 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(require_pin=False) config["pin_hash"] = pin_hash self.save_config(config) def verify_pin(self, pin: str) -> bool: """Check a provided PIN against the stored hash without prompting.""" config = self.load_config(require_pin=False) stored = config.get("pin_hash", "").encode() if not stored: return False return bcrypt.checkpw(pin.encode(), stored) def change_pin(self, old_pin: str, new_pin: str) -> bool: """Update the stored PIN if the old PIN is correct.""" if self.verify_pin(old_pin): self.set_pin(new_pin) return True return False