diff --git a/src/seedpass/core/manager.py b/src/seedpass/core/manager.py index dd02f64b..c0885b65 100644 --- a/src/seedpass/core/manager.py +++ b/src/seedpass/core/manager.py @@ -353,14 +353,18 @@ class PasswordManager: self.KEY_TOTP_DET = kd(master, b"seedpass:v1:totp") def _get_kdf_mode(self) -> str: - if getattr(self, "config_manager", None): - return self.config_manager.get_kdf_mode() + cfg_mgr = getattr(self, "config_manager", None) + getter = getattr(cfg_mgr, "get_kdf_mode", None) if cfg_mgr else None + if callable(getter): + return getter() return "pbkdf2" def _get_kdf_iterations(self) -> int: floor = getattr(ConfigManager, "DEFAULT_PBKDF2_ITERATIONS", 200_000) - if getattr(self, "config_manager", None): - configured = int(self.config_manager.get_kdf_iterations()) + cfg_mgr = getattr(self, "config_manager", None) + getter = getattr(cfg_mgr, "get_kdf_iterations", None) if cfg_mgr else None + if callable(getter): + configured = int(getter()) if configured < floor: logger.warning( "Configured PBKDF2 iterations (%s) is below policy floor (%s); using floor.", @@ -371,8 +375,10 @@ class PasswordManager: return floor def _get_argon2_time_cost(self) -> int: - if getattr(self, "config_manager", None): - return self.config_manager.get_argon2_time_cost() + cfg_mgr = getattr(self, "config_manager", None) + getter = getattr(cfg_mgr, "get_argon2_time_cost", None) if cfg_mgr else None + if callable(getter): + return getter() return 2 def _derive_seed_key( @@ -436,6 +442,28 @@ class PasswordManager: salt_b64=salt_b64, ) + def _encrypt_parent_seed_compat( + self, + seed_mgr: EncryptionManager, + seed: str, + fingerprint: str, + *, + mode: str | None = None, + ) -> None: + """Encrypt parent seed with metadata, falling back for legacy implementations.""" + + try: + seed_mgr.encrypt_parent_seed( + seed, + kdf=self._build_seed_kdf_config(fingerprint, mode=mode), + ) + except TypeError: + logger.warning( + "EncryptionManager.encrypt_parent_seed does not accept kdf metadata; " + "falling back to legacy call signature." + ) + seed_mgr.encrypt_parent_seed(seed) + def ensure_key_hierarchy(self) -> None: """Ensure sub-keys are derived from the current parent seed.""" if ( @@ -768,9 +796,39 @@ class PasswordManager: # Ensure managers are initialized for the newly created profile if getattr(self, "config_manager", None) is None: if getattr(self, "encryption_manager", None) is None: - if hasattr(self.fingerprint_manager, "select_fingerprint"): - self.select_fingerprint(fingerprint) + has_select = hasattr(self.fingerprint_manager, "select_fingerprint") + if hasattr(self.fingerprint_manager, "list_fingerprints"): + fingerprints = set( + self.fingerprint_manager.list_fingerprints() + ) else: + fingerprints = set() + if ( + fingerprint in fingerprints + and has_select + ): + self.select_fingerprint(fingerprint) + elif has_select and not fingerprints: + # If state is already staged for the new profile, initialize + # directly; otherwise prefer selecting. + if getattr(self, "fingerprint_dir", None) is not None: + self.current_fingerprint = fingerprint + self.initialize_managers() + else: + self.select_fingerprint(fingerprint) + else: + # Some flows can pre-populate current profile state before + # persistence catches up; initialize managers from current state. + self.current_fingerprint = fingerprint + if getattr(self, "fingerprint_dir", None) is None: + if hasattr(self.fingerprint_manager, "get_fingerprint_dir"): + self.fingerprint_dir = ( + self.fingerprint_manager.get_fingerprint_dir( + fingerprint + ) + ) + else: + self.fingerprint_dir = None self.initialize_managers() else: self.initialize_managers() @@ -906,12 +964,11 @@ class PasswordManager: fingerprint_dir=fingerprint_dir, ) - seed_mgr.encrypt_parent_seed( + self._encrypt_parent_seed_compat( + seed_mgr, parent_seed, - kdf=self._build_seed_kdf_config( - fingerprint, - mode=self._get_kdf_mode(), - ), + fingerprint, + mode=self._get_kdf_mode(), ) self.store_hashed_password(password) @@ -1369,10 +1426,7 @@ class PasswordManager: fingerprint_dir=fingerprint_dir, ) - seed_mgr.encrypt_parent_seed( - parent_seed, - kdf=self._build_seed_kdf_config(fingerprint), - ) + self._encrypt_parent_seed_compat(seed_mgr, parent_seed, fingerprint) logging.info("Parent seed encrypted and saved successfully.") self.store_hashed_password(password) @@ -1567,12 +1621,11 @@ class PasswordManager: self.store_hashed_password(password) logging.info("User password hashed and stored successfully.") - seed_mgr.encrypt_parent_seed( + self._encrypt_parent_seed_compat( + seed_mgr, seed, - kdf=self._build_seed_kdf_config( - fingerprint_dir.name, - mode=self._get_kdf_mode(), - ), + fingerprint_dir.name, + mode=self._get_kdf_mode(), ) logging.info("Parent seed encrypted and saved successfully.") @@ -4532,9 +4585,8 @@ class PasswordManager: password = prompt_new_password() seed_key = self._derive_seed_key(password, self.current_fingerprint) seed_mgr = EncryptionManager(seed_key, self.fingerprint_dir) - seed_mgr.encrypt_parent_seed( - self.parent_seed, - kdf=self._build_seed_kdf_config(self.current_fingerprint), + self._encrypt_parent_seed_compat( + seed_mgr, self.parent_seed, self.current_fingerprint ) self.store_hashed_password(password) except Exception as e: @@ -4837,9 +4889,8 @@ class PasswordManager: new_enc_mgr = EncryptionManager(new_key, self.fingerprint_dir) - seed_mgr.encrypt_parent_seed( - self.parent_seed, - kdf=self._build_seed_kdf_config(self.current_fingerprint), + self._encrypt_parent_seed_compat( + seed_mgr, self.parent_seed, self.current_fingerprint ) self.vault.set_encryption_manager(new_enc_mgr) self.vault.save_index(index_data) diff --git a/src/seedpass_gui/app.py b/src/seedpass_gui/app.py index f7543351..0b06153d 100644 --- a/src/seedpass_gui/app.py +++ b/src/seedpass_gui/app.py @@ -398,19 +398,22 @@ class TotpViewerWindow(toga.Window): ): entry = self.entries.retrieve_entry(idx) - if idx in self._secret_cache: - secret = self._secret_cache[idx] - else: - try: - secret = self.entries.get_totp_secret(idx) - self._secret_cache[idx] = secret - except Exception: - # Skip entries where secret cannot be derived (e.g. no seed) - continue - - code = TotpManager.current_code_from_secret(secret) period = int(entry.get("period", 30)) if entry else 30 remaining = TotpManager.time_remaining(period) + if hasattr(self.entries, "get_totp_secret"): + if idx in self._secret_cache: + secret = self._secret_cache[idx] + else: + try: + secret = self.entries.get_totp_secret(idx) + self._secret_cache[idx] = secret + except Exception: + # Skip entries where secret cannot be derived (e.g. no seed) + continue + code = TotpManager.current_code_from_secret(secret) + else: + # Compatibility path for services exposing code retrieval directly. + code = self.entries.get_totp_code(idx) self.table.data.append((label, code, remaining)) def cleanup(self, *args: object, **kwargs: object) -> None: