From 3b27a393a5c5ef232e890df25bb368120a856a24 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 30 Jun 2025 09:34:04 -0400 Subject: [PATCH] store password hash in config --- src/password_manager/config_manager.py | 20 ++++++++++- src/password_manager/manager.py | 47 +++++++++++++++++--------- src/tests/test_config_manager.py | 26 +++++++++++++- 3 files changed, 75 insertions(+), 18 deletions(-) diff --git a/src/password_manager/config_manager.py b/src/password_manager/config_manager.py index a45794c..b64842b 100644 --- a/src/password_manager/config_manager.py +++ b/src/password_manager/config_manager.py @@ -37,7 +37,11 @@ class ConfigManager: """ if not self.config_path.exists(): logger.info("Config file not found; returning defaults") - return {"relays": list(DEFAULT_NOSTR_RELAYS), "pin_hash": ""} + return { + "relays": list(DEFAULT_NOSTR_RELAYS), + "pin_hash": "", + "password_hash": "", + } try: data = self.vault.load_config() if not isinstance(data, dict): @@ -45,6 +49,14 @@ class ConfigManager: # Ensure defaults for missing keys data.setdefault("relays", list(DEFAULT_NOSTR_RELAYS)) data.setdefault("pin_hash", "") + data.setdefault("password_hash", "") + + # Migrate legacy hashed_password.enc if present and password_hash is missing + legacy_file = self.fingerprint_dir / "hashed_password.enc" + if not data.get("password_hash") and legacy_file.exists(): + with open(legacy_file, "rb") as f: + data["password_hash"] = f.read().decode() + self.save_config(data) if require_pin and data.get("pin_hash"): for _ in range(3): pin = getpass.getpass("Enter settings PIN: ").strip() @@ -95,3 +107,9 @@ class ConfigManager: self.set_pin(new_pin) return True return False + + def set_password_hash(self, password_hash: str) -> None: + """Persist the bcrypt password hash in the config.""" + config = self.load_config(require_pin=False) + config["password_hash"] = password_hash + self.save_config(config) diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index 241fe3e..c4a0494 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -1164,13 +1164,20 @@ class PasswordManager: bool: True if the password is correct, False otherwise. """ try: - hashed_password_file = self.fingerprint_dir / "hashed_password.enc" - if not hashed_password_file.exists(): - logging.error("Hashed password file not found.") - print(colored("Error: Hashed password file not found.", "red")) - return False - with open(hashed_password_file, "rb") as f: - stored_hash = f.read() + config = self.config_manager.load_config(require_pin=False) + stored_hash = config.get("password_hash", "").encode() + if not stored_hash: + # Fallback to legacy file if hash not present in config + legacy_file = self.fingerprint_dir / "hashed_password.enc" + if legacy_file.exists(): + with open(legacy_file, "rb") as f: + stored_hash = f.read() + self.config_manager.set_password_hash(stored_hash.decode()) + else: + logging.error("Hashed password not found.") + print(colored("Error: Hashed password not found.", "red")) + return False + is_correct = bcrypt.checkpw(password.encode("utf-8"), stored_hash) if is_correct: logging.debug("Password verification successful.") @@ -1206,19 +1213,27 @@ class PasswordManager: This should be called during the initial setup. """ try: - hashed_password_file = self.fingerprint_dir / "hashed_password.enc" - hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()) - with open(hashed_password_file, "wb") as f: - f.write(hashed) - os.chmod(hashed_password_file, 0o600) + hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode() + if self.config_manager: + self.config_manager.set_password_hash(hashed) + else: + # Fallback to legacy file method if config_manager unavailable + hashed_password_file = self.fingerprint_dir / "hashed_password.enc" + with open(hashed_password_file, "wb") as f: + f.write(hashed.encode()) + os.chmod(hashed_password_file, 0o600) logging.info("User password hashed and stored successfully.") except AttributeError: # If bcrypt.hashpw is not available, try using bcrypt directly salt = bcrypt.gensalt() - hashed = bcrypt.hashpw(password.encode("utf-8"), salt) - with open(hashed_password_file, "wb") as f: - f.write(hashed) - os.chmod(hashed_password_file, 0o600) + hashed = bcrypt.hashpw(password.encode("utf-8"), salt).decode() + if self.config_manager: + self.config_manager.set_password_hash(hashed) + else: + hashed_password_file = self.fingerprint_dir / "hashed_password.enc" + with open(hashed_password_file, "wb") as f: + f.write(hashed.encode()) + os.chmod(hashed_password_file, 0o600) logging.info( "User password hashed and stored successfully (using alternative method)." ) diff --git a/src/tests/test_config_manager.py b/src/tests/test_config_manager.py index 49a27ef..c64a7e7 100644 --- a/src/tests/test_config_manager.py +++ b/src/tests/test_config_manager.py @@ -23,6 +23,7 @@ def test_config_defaults_and_round_trip(): cfg = cfg_mgr.load_config(require_pin=False) assert cfg["relays"] == list(DEFAULT_RELAYS) assert cfg["pin_hash"] == "" + assert cfg["password_hash"] == "" cfg_mgr.set_pin("1234") cfg_mgr.set_relays(["wss://example.com"], require_pin=False) @@ -64,7 +65,9 @@ def test_config_file_encrypted_after_save(): assert raw != json.dumps(data).encode() loaded = cfg_mgr.load_config(require_pin=False) - assert loaded == data + assert loaded["relays"] == data["relays"] + assert loaded["pin_hash"] == data["pin_hash"] + assert loaded["password_hash"] == "" def test_set_relays_persists_changes(): @@ -86,3 +89,24 @@ def test_set_relays_requires_at_least_one(): cfg_mgr = ConfigManager(vault, Path(tmpdir)) with pytest.raises(ValueError): cfg_mgr.set_relays([], require_pin=False) + + +def test_password_hash_migrates_from_file(tmp_path): + key = Fernet.generate_key() + enc_mgr = EncryptionManager(key, tmp_path) + vault = Vault(enc_mgr, tmp_path) + cfg_mgr = ConfigManager(vault, tmp_path) + + # save legacy config without password_hash + legacy_cfg = {"relays": ["wss://r"], "pin_hash": ""} + cfg_mgr.save_config(legacy_cfg) + + hashed = bcrypt.hashpw(b"pw", bcrypt.gensalt()) + (tmp_path / "hashed_password.enc").write_bytes(hashed) + + cfg = cfg_mgr.load_config(require_pin=False) + assert cfg["password_hash"] == hashed.decode() + # subsequent loads should read from config + (tmp_path / "hashed_password.enc").unlink() + cfg2 = cfg_mgr.load_config(require_pin=False) + assert cfg2["password_hash"] == hashed.decode()