Merge pull request #140 from PR0M3TH3AN/codex/add-adjustable-idle-timeout-for-inactivity-lock

Add configurable inactivity timeout
This commit is contained in:
thePR0M3TH3AN
2025-07-02 14:08:21 -04:00
committed by GitHub
4 changed files with 78 additions and 4 deletions

View File

@@ -379,6 +379,40 @@ def handle_reset_relays(password_manager: PasswordManager) -> None:
print(colored(f"Error: {e}", "red")) 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: def handle_profiles_menu(password_manager: PasswordManager) -> None:
"""Submenu for managing seed profiles.""" """Submenu for managing seed profiles."""
while True: while True:
@@ -461,8 +495,9 @@ def handle_settings(password_manager: PasswordManager) -> None:
print("6. Backup Parent Seed") print("6. Backup Parent Seed")
print("7. Export database") print("7. Export database")
print("8. Import database") print("8. Import database")
print("9. Lock Vault") print("9. Set inactivity timeout")
print("10. Back") print("10. Lock Vault")
print("11. Back")
choice = input("Select an option: ").strip() choice = input("Select an option: ").strip()
if choice == "1": if choice == "1":
handle_profiles_menu(password_manager) handle_profiles_menu(password_manager)
@@ -488,10 +523,12 @@ def handle_settings(password_manager: PasswordManager) -> None:
if path: if path:
password_manager.handle_import_database(Path(path)) password_manager.handle_import_database(Path(path))
elif choice == "9": elif choice == "9":
handle_set_inactivity_timeout(password_manager)
elif choice == "10":
password_manager.lock_vault() password_manager.lock_vault()
print(colored("Vault locked. Please re-enter your password.", "yellow")) print(colored("Vault locked. Please re-enter your password.", "yellow"))
password_manager.unlock_vault() password_manager.unlock_vault()
elif choice == "10": elif choice == "11":
break break
else: else:
print(colored("Invalid choice.", "red")) print(colored("Invalid choice.", "red"))
@@ -651,7 +688,9 @@ if __name__ == "__main__":
# Display the interactive menu to the user # Display the interactive menu to the user
try: try:
display_menu(password_manager) display_menu(
password_manager, inactivity_timeout=password_manager.inactivity_timeout
)
except KeyboardInterrupt: except KeyboardInterrupt:
logger.info("Program terminated by user via KeyboardInterrupt.") logger.info("Program terminated by user via KeyboardInterrupt.")
print(colored("\nProgram terminated by user.", "yellow")) print(colored("\nProgram terminated by user.", "yellow"))

View File

@@ -16,6 +16,7 @@ from utils.key_derivation import (
EncryptionMode, EncryptionMode,
DEFAULT_ENCRYPTION_MODE, DEFAULT_ENCRYPTION_MODE,
) )
from constants import INACTIVITY_TIMEOUT
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -46,6 +47,7 @@ class ConfigManager:
"pin_hash": "", "pin_hash": "",
"password_hash": "", "password_hash": "",
"encryption_mode": DEFAULT_ENCRYPTION_MODE.value, "encryption_mode": DEFAULT_ENCRYPTION_MODE.value,
"inactivity_timeout": INACTIVITY_TIMEOUT,
} }
try: try:
data = self.vault.load_config() data = self.vault.load_config()
@@ -56,6 +58,7 @@ class ConfigManager:
data.setdefault("pin_hash", "") data.setdefault("pin_hash", "")
data.setdefault("password_hash", "") data.setdefault("password_hash", "")
data.setdefault("encryption_mode", DEFAULT_ENCRYPTION_MODE.value) 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 # Migrate legacy hashed_password.enc if present and password_hash is missing
legacy_file = self.fingerprint_dir / "hashed_password.enc" legacy_file = self.fingerprint_dir / "hashed_password.enc"
@@ -125,3 +128,16 @@ class ConfigManager:
config = self.load_config(require_pin=False) config = self.load_config(require_pin=False)
config["encryption_mode"] = mode.value config["encryption_mode"] = mode.value
self.save_config(config) 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))

View File

@@ -50,6 +50,7 @@ from constants import (
MIN_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH,
MAX_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH,
DEFAULT_PASSWORD_LENGTH, DEFAULT_PASSWORD_LENGTH,
INACTIVITY_TIMEOUT,
DEFAULT_SEED_BACKUP_FILENAME, DEFAULT_SEED_BACKUP_FILENAME,
) )
@@ -101,6 +102,7 @@ class PasswordManager:
self.last_update: float = time.time() self.last_update: float = time.time()
self.last_activity: float = time.time() self.last_activity: float = time.time()
self.locked: bool = False self.locked: bool = False
self.inactivity_timeout: float = INACTIVITY_TIMEOUT
# Initialize the fingerprint manager first # Initialize the fingerprint manager first
self.initialize_fingerprint_manager() self.initialize_fingerprint_manager()
@@ -786,6 +788,9 @@ class PasswordManager:
) )
config = self.config_manager.load_config() config = self.config_manager.load_config()
relay_list = config.get("relays", list(DEFAULT_RELAYS)) relay_list = config.get("relays", list(DEFAULT_RELAYS))
self.inactivity_timeout = config.get(
"inactivity_timeout", INACTIVITY_TIMEOUT
)
self.nostr_client = NostrClient( self.nostr_client = NostrClient(
encryption_manager=self.encryption_manager, encryption_manager=self.encryption_manager,

View File

@@ -10,6 +10,7 @@ sys.path.append(str(Path(__file__).resolve().parents[1]))
from password_manager.config_manager import ConfigManager from password_manager.config_manager import ConfigManager
from password_manager.vault import Vault from password_manager.vault import Vault
from nostr.client import DEFAULT_RELAYS from nostr.client import DEFAULT_RELAYS
from constants import INACTIVITY_TIMEOUT
def test_config_defaults_and_round_trip(): 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) 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): def test_password_hash_migrates_from_file(tmp_path):
vault, enc_mgr = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD) vault, enc_mgr = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD)
cfg_mgr = ConfigManager(vault, tmp_path) cfg_mgr = ConfigManager(vault, tmp_path)