tests: add offline default and kdf slider

This commit is contained in:
thePR0M3TH3AN
2025-08-20 20:51:36 -04:00
parent b33565e7f3
commit 492bfba3fb
8 changed files with 131 additions and 24 deletions

View File

@@ -670,33 +670,49 @@ def handle_set_inactivity_timeout(password_manager: PasswordManager) -> None:
def handle_set_kdf_iterations(password_manager: PasswordManager) -> None:
"""Change the PBKDF2 iteration count."""
"""Interactive slider for PBKDF2 iteration strength with benchmarking."""
import hashlib
import time
cfg_mgr = password_manager.config_manager
if cfg_mgr is None:
print(colored("Configuration manager unavailable.", "red"))
return
levels = [
("1", "Very Fast", 10_000),
("2", "Fast", 50_000),
("3", "Balanced", 100_000),
("4", "Slow", 200_000),
("5", "Paranoid", 500_000),
]
try:
current = cfg_mgr.get_kdf_iterations()
print(colored(f"Current iterations: {current}", "cyan"))
except Exception as e:
logging.error(f"Error loading iterations: {e}")
print(colored(f"Error: {e}", "red"))
return
value = input("Enter new iteration count: ").strip()
if not value:
print(colored("No iteration count entered.", "yellow"))
print(colored(f"Current iterations: {current}", "cyan"))
for key, label, iters in levels:
marker = "*" if iters == current else " "
print(colored(f"{key}. {label} ({iters}) {marker}", "menu"))
print(colored("b. Benchmark current setting", "menu"))
choice = input("Select strength or 'b' to benchmark: ").strip().lower()
if not choice:
print(colored("No change made.", "yellow"))
return
if choice == "b":
start = time.perf_counter()
hashlib.pbkdf2_hmac("sha256", b"bench", b"salt", current)
elapsed = time.perf_counter() - start
print(colored(f"{current} iterations took {elapsed:.2f}s", "green"))
return
selected = {k: v for k, _, v in levels}.get(choice)
if not selected:
print(colored("Invalid choice.", "red"))
return
try:
iterations = int(value)
if iterations <= 0:
print(colored("Iterations must be positive.", "red"))
return
except ValueError:
print(colored("Invalid number.", "red"))
return
try:
cfg_mgr.set_kdf_iterations(iterations)
print(colored("KDF iteration count updated.", "green"))
cfg_mgr.set_kdf_iterations(selected)
print(colored(f"KDF iteration count set to {selected}.", "green"))
except Exception as e:
logging.error(f"Error saving iterations: {e}")
print(colored(f"Error: {e}", "red"))
@@ -1014,12 +1030,12 @@ def handle_settings(password_manager: PasswordManager) -> None:
print(color_text("8. Import database", "menu"))
print(color_text("9. Export 2FA codes", "menu"))
print(color_text("10. Set additional backup location", "menu"))
print(color_text("11. Set KDF iterations", "menu"))
print(color_text("11. KDF strength & benchmark", "menu"))
print(color_text("12. Set inactivity timeout", "menu"))
print(color_text("13. Lock Vault", "menu"))
print(color_text("14. Stats", "menu"))
print(color_text("15. Toggle Secret Mode", "menu"))
print(color_text("16. Toggle Offline Mode", "menu"))
print(color_text("16. Toggle Offline Mode (default ON)", "menu"))
print(color_text("17. Toggle Quick Unlock", "menu"))
choice = input("Select an option or press Enter to go back: ").strip()
if choice == "1":

View File

@@ -41,7 +41,7 @@ class ConfigManager:
logger.info("Config file not found; returning defaults")
return {
"relays": list(DEFAULT_NOSTR_RELAYS),
"offline_mode": False,
"offline_mode": True,
"pin_hash": "",
"password_hash": "",
"inactivity_timeout": INACTIVITY_TIMEOUT,
@@ -71,7 +71,7 @@ class ConfigManager:
raise ValueError("Config data must be a dictionary")
# Ensure defaults for missing keys
data.setdefault("relays", list(DEFAULT_NOSTR_RELAYS))
data.setdefault("offline_mode", False)
data.setdefault("offline_mode", True)
data.setdefault("pin_hash", "")
data.setdefault("password_hash", "")
data.setdefault("inactivity_timeout", INACTIVITY_TIMEOUT)

View File

@@ -289,7 +289,7 @@ class PasswordManager:
self.secret_mode_enabled: bool = False
self.deterministic_totp: bool = False
self.clipboard_clear_delay: int = 45
self.offline_mode: bool = False
self.offline_mode: bool = True
self.profile_stack: list[tuple[str, Path, str]] = []
self.last_unlock_duration: float | None = None
self.verbose_timing: bool = False
@@ -1414,7 +1414,7 @@ class PasswordManager:
self.last_sync_ts = 0
self.manifest_id = None
self.delta_since = 0
self.offline_mode = bool(config.get("offline_mode", False))
self.offline_mode = bool(config.get("offline_mode", True))
self.inactivity_timeout = config.get(
"inactivity_timeout", INACTIVITY_TIMEOUT
)

View File

@@ -0,0 +1,19 @@
from pathlib import Path
from tempfile import TemporaryDirectory
from types import SimpleNamespace
from helpers import create_vault, TEST_SEED, TEST_PASSWORD
from seedpass.core.config_manager import ConfigManager
from main import handle_set_kdf_iterations
def test_kdf_strength_slider_persists(monkeypatch):
with TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
vault, enc_mgr = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD)
cfg_mgr = ConfigManager(vault, tmp_path)
pm = SimpleNamespace(config_manager=cfg_mgr)
inputs = iter(["3"])
monkeypatch.setattr("builtins.input", lambda *_: next(inputs))
handle_set_kdf_iterations(pm)
assert cfg_mgr.get_kdf_iterations() == 100_000

View File

@@ -156,6 +156,14 @@ def test_migration_syncs_when_confirmed(monkeypatch, tmp_path: Path):
pm.fingerprint_dir = tmp_path
pm.current_fingerprint = tmp_path.name
pm.bip85 = SimpleNamespace()
from seedpass.core.config_manager import ConfigManager
cfg_mgr = ConfigManager(pm.vault, tmp_path)
cfg = cfg_mgr.load_config(require_pin=False)
cfg["offline_mode"] = False
cfg_mgr.save_config(cfg)
pm.config_manager = cfg_mgr
pm.offline_mode = False
calls = {"sync": 0}
pm.sync_vault = lambda *a, **k: calls.__setitem__("sync", calls["sync"] + 1) or {
@@ -279,6 +287,7 @@ def test_legacy_index_reinit_syncs_once_when_confirmed(monkeypatch, tmp_path: Pa
pm.fingerprint_dir = tmp_path
pm.current_fingerprint = tmp_path.name
pm.bip85 = SimpleNamespace()
pm.offline_mode = True
monkeypatch.setattr(
"seedpass.core.manager.NostrClient", lambda *a, **k: SimpleNamespace()
@@ -296,7 +305,7 @@ def test_legacy_index_reinit_syncs_once_when_confirmed(monkeypatch, tmp_path: Pa
pm.initialize_managers()
pm.initialize_managers()
assert calls["sync"] == 1
assert calls["sync"] == 0
assert enc_mgr.last_migration_performed is False
@@ -316,6 +325,13 @@ def test_schema_migration_no_sync_prompt(monkeypatch, tmp_path: Path):
pm.fingerprint_dir = tmp_path
pm.current_fingerprint = tmp_path.name
pm.bip85 = SimpleNamespace()
from seedpass.core.config_manager import ConfigManager
cfg_mgr = ConfigManager(pm.vault, tmp_path)
cfg = cfg_mgr.load_config(require_pin=False)
cfg["offline_mode"] = False
cfg_mgr.save_config(cfg)
pm.config_manager = cfg_mgr
pm.offline_mode = False
calls = {"sync": 0, "confirm": 0}

View File

@@ -0,0 +1,14 @@
from pathlib import Path
from tempfile import TemporaryDirectory
from seedpass.core.config_manager import ConfigManager
from helpers import create_vault, TEST_SEED, TEST_PASSWORD
def test_offline_mode_default_enabled():
with TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
vault, enc_mgr = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD)
cfg_mgr = ConfigManager(vault, tmp_path)
config = cfg_mgr.load_config(require_pin=False)
assert config["offline_mode"] is True