diff --git a/src/main.py b/src/main.py index e67dba8..3ba849f 100644 --- a/src/main.py +++ b/src/main.py @@ -379,6 +379,40 @@ def handle_reset_relays(password_manager: PasswordManager) -> None: print(colored(f"Error: {e}", "red")) +def handle_set_inactivity_timeout(password_manager: PasswordManager) -> None: + """Change the inactivity timeout for the current seed profile.""" + cfg_mgr = password_manager.config_manager + if cfg_mgr is None: + print(colored("Configuration manager unavailable.", "red")) + return + try: + current = cfg_mgr.get_inactivity_timeout() / 60 + print(colored(f"Current timeout: {current:.1f} minutes", "cyan")) + except Exception as e: + logging.error(f"Error loading timeout: {e}") + print(colored(f"Error: {e}", "red")) + return + value = input("Enter new timeout in minutes: ").strip() + if not value: + print(colored("No timeout entered.", "yellow")) + return + try: + minutes = float(value) + if minutes <= 0: + print(colored("Timeout must be positive.", "red")) + return + except ValueError: + print(colored("Invalid number.", "red")) + return + try: + cfg_mgr.set_inactivity_timeout(minutes * 60) + password_manager.inactivity_timeout = minutes * 60 + print(colored("Inactivity timeout updated.", "green")) + except Exception as e: + logging.error(f"Error saving timeout: {e}") + print(colored(f"Error: {e}", "red")) + + def handle_profiles_menu(password_manager: PasswordManager) -> None: """Submenu for managing seed profiles.""" while True: @@ -461,8 +495,9 @@ def handle_settings(password_manager: PasswordManager) -> None: print("6. Backup Parent Seed") print("7. Export database") print("8. Import database") - print("9. Lock Vault") - print("10. Back") + print("9. Set inactivity timeout") + print("10. Lock Vault") + print("11. Back") choice = input("Select an option: ").strip() if choice == "1": handle_profiles_menu(password_manager) @@ -488,10 +523,12 @@ def handle_settings(password_manager: PasswordManager) -> None: if path: password_manager.handle_import_database(Path(path)) elif choice == "9": + handle_set_inactivity_timeout(password_manager) + elif choice == "10": password_manager.lock_vault() print(colored("Vault locked. Please re-enter your password.", "yellow")) password_manager.unlock_vault() - elif choice == "10": + elif choice == "11": break else: print(colored("Invalid choice.", "red")) @@ -651,7 +688,9 @@ if __name__ == "__main__": # Display the interactive menu to the user try: - display_menu(password_manager) + display_menu( + password_manager, inactivity_timeout=password_manager.inactivity_timeout + ) except KeyboardInterrupt: logger.info("Program terminated by user via KeyboardInterrupt.") print(colored("\nProgram terminated by user.", "yellow")) diff --git a/src/password_manager/config_manager.py b/src/password_manager/config_manager.py index ac4b46a..eb689fb 100644 --- a/src/password_manager/config_manager.py +++ b/src/password_manager/config_manager.py @@ -16,6 +16,7 @@ from utils.key_derivation import ( EncryptionMode, DEFAULT_ENCRYPTION_MODE, ) +from constants import INACTIVITY_TIMEOUT logger = logging.getLogger(__name__) @@ -46,6 +47,7 @@ class ConfigManager: "pin_hash": "", "password_hash": "", "encryption_mode": DEFAULT_ENCRYPTION_MODE.value, + "inactivity_timeout": INACTIVITY_TIMEOUT, } try: data = self.vault.load_config() @@ -56,6 +58,7 @@ class ConfigManager: data.setdefault("pin_hash", "") data.setdefault("password_hash", "") data.setdefault("encryption_mode", DEFAULT_ENCRYPTION_MODE.value) + data.setdefault("inactivity_timeout", INACTIVITY_TIMEOUT) # Migrate legacy hashed_password.enc if present and password_hash is missing legacy_file = self.fingerprint_dir / "hashed_password.enc" @@ -125,3 +128,16 @@ class ConfigManager: config = self.load_config(require_pin=False) config["encryption_mode"] = mode.value self.save_config(config) + + def set_inactivity_timeout(self, timeout_seconds: float) -> None: + """Persist the inactivity timeout in seconds.""" + if timeout_seconds <= 0: + raise ValueError("Timeout must be positive") + config = self.load_config(require_pin=False) + config["inactivity_timeout"] = timeout_seconds + self.save_config(config) + + def get_inactivity_timeout(self) -> float: + """Retrieve the inactivity timeout setting in seconds.""" + config = self.load_config(require_pin=False) + return float(config.get("inactivity_timeout", INACTIVITY_TIMEOUT)) diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index d2886a7..7c7ffb6 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -50,6 +50,7 @@ from constants import ( MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH, DEFAULT_PASSWORD_LENGTH, + INACTIVITY_TIMEOUT, DEFAULT_SEED_BACKUP_FILENAME, ) @@ -101,6 +102,7 @@ class PasswordManager: self.last_update: float = time.time() self.last_activity: float = time.time() self.locked: bool = False + self.inactivity_timeout: float = INACTIVITY_TIMEOUT # Initialize the fingerprint manager first self.initialize_fingerprint_manager() @@ -786,6 +788,9 @@ class PasswordManager: ) config = self.config_manager.load_config() relay_list = config.get("relays", list(DEFAULT_RELAYS)) + self.inactivity_timeout = config.get( + "inactivity_timeout", INACTIVITY_TIMEOUT + ) self.nostr_client = NostrClient( encryption_manager=self.encryption_manager, diff --git a/src/tests/test_config_manager.py b/src/tests/test_config_manager.py index 7433dba..92f3f31 100644 --- a/src/tests/test_config_manager.py +++ b/src/tests/test_config_manager.py @@ -10,6 +10,7 @@ sys.path.append(str(Path(__file__).resolve().parents[1])) from password_manager.config_manager import ConfigManager from password_manager.vault import Vault from nostr.client import DEFAULT_RELAYS +from constants import INACTIVITY_TIMEOUT def test_config_defaults_and_round_trip(): @@ -80,6 +81,19 @@ def test_set_relays_requires_at_least_one(): cfg_mgr.set_relays([], require_pin=False) +def test_inactivity_timeout_round_trip(): + with TemporaryDirectory() as tmpdir: + vault, enc_mgr = create_vault(Path(tmpdir), TEST_SEED, TEST_PASSWORD) + cfg_mgr = ConfigManager(vault, Path(tmpdir)) + + cfg = cfg_mgr.load_config(require_pin=False) + assert cfg["inactivity_timeout"] == INACTIVITY_TIMEOUT + + cfg_mgr.set_inactivity_timeout(123) + cfg2 = cfg_mgr.load_config(require_pin=False) + assert cfg2["inactivity_timeout"] == 123 + + def test_password_hash_migrates_from_file(tmp_path): vault, enc_mgr = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD) cfg_mgr = ConfigManager(vault, tmp_path)