From d2832db194c55b02aa6839f113e90622639600a2 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 2 Jul 2025 18:48:15 -0400 Subject: [PATCH] Remove password-based encryption modes --- src/main.py | 9 +- src/password_manager/manager.py | 54 ++------- src/password_manager/portable_backup.py | 30 ++--- src/tests/helpers.py | 4 +- src/tests/test_cli_encryption_mode.py | 20 ---- .../test_cli_portable_backup_commands.py | 9 +- src/tests/test_concurrency_stress.py | 8 +- src/tests/test_encryption_mode_change.py | 57 --------- src/tests/test_encryption_mode_migration.py | 92 -------------- src/tests/test_index_import_export.py | 22 +--- src/tests/test_key_derivation.py | 23 +--- src/tests/test_password_change.py | 2 +- .../test_password_unlock_after_change.py | 4 +- src/tests/test_portable_backup.py | 112 ++++-------------- src/utils/key_derivation.py | 37 +----- 15 files changed, 56 insertions(+), 427 deletions(-) delete mode 100644 src/tests/test_encryption_mode_change.py delete mode 100644 src/tests/test_encryption_mode_migration.py diff --git a/src/main.py b/src/main.py index d1bec58..ef6a590 100644 --- a/src/main.py +++ b/src/main.py @@ -15,7 +15,6 @@ from termcolor import colored import traceback from password_manager.manager import PasswordManager -from password_manager.portable_backup import PortableMode from nostr.client import NostrClient from constants import INACTIVITY_TIMEOUT from utils.key_derivation import EncryptionMode @@ -630,11 +629,6 @@ if __name__ == "__main__": ) exp = sub.add_parser("export") - exp.add_argument( - "--mode", - choices=[m.value for m in PortableMode], - default=PortableMode.SEED_ONLY.value, - ) exp.add_argument("--file") imp = sub.add_parser("import") @@ -662,8 +656,7 @@ if __name__ == "__main__": sys.exit(1) if args.command == "export": - mode = PortableMode(args.mode) - password_manager.handle_export_database(mode, Path(args.file)) + password_manager.handle_export_database(Path(args.file)) sys.exit(0) elif args.command == "import": password_manager.handle_import_database(Path(args.file)) diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index 59ad46d..9233cd5 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -24,11 +24,7 @@ from password_manager.entry_management import EntryManager from password_manager.password_generation import PasswordGenerator from password_manager.backup import BackupManager from password_manager.vault import Vault -from password_manager.portable_backup import ( - export_backup, - import_backup, - PortableMode, -) +from password_manager.portable_backup import export_backup, import_backup from utils.key_derivation import ( derive_key_from_parent_seed, derive_key_from_password, @@ -125,22 +121,7 @@ class PasswordManager: Returns: EncryptionMode: The chosen encryption mode. """ - print("Choose encryption mode [Enter for seed-only]") - print(" 1) seed-only") - print(" 2) seed+password") - print(" 3) password-only (legacy)") - mode_choice = input("Select option: ").strip() - - if mode_choice == "2": - return EncryptionMode.SEED_PLUS_PW - elif mode_choice == "3": - print( - colored( - "⚠️ Password-only encryption is less secure and not recommended.", - "yellow", - ) - ) - return EncryptionMode.PW_ONLY + # Only seed-only mode is supported return EncryptionMode.SEED_ONLY def lock_vault(self) -> None: @@ -309,11 +290,7 @@ class PasswordManager: sys.exit(1) return False - key = derive_index_key( - self.parent_seed, - password, - self.encryption_mode, - ) + key = derive_index_key(self.parent_seed) self.encryption_manager = EncryptionManager(key, fingerprint_dir) self.vault = Vault(self.encryption_manager, fingerprint_dir) @@ -566,11 +543,7 @@ class PasswordManager: # Initialize EncryptionManager with key and fingerprint_dir password = prompt_for_password() - index_key = derive_index_key( - parent_seed, - password, - self.encryption_mode, - ) + index_key = derive_index_key(parent_seed) seed_key = derive_key_from_password(password) self.encryption_manager = EncryptionManager(index_key, fingerprint_dir) @@ -707,11 +680,7 @@ class PasswordManager: # Prompt for password password = prompt_for_password() - index_key = derive_index_key( - seed, - password, - self.encryption_mode, - ) + index_key = derive_index_key(seed) seed_key = derive_key_from_password(password) self.encryption_manager = EncryptionManager(index_key, fingerprint_dir) @@ -1225,7 +1194,6 @@ class PasswordManager: def handle_export_database( self, - mode: "PortableMode" = PortableMode.SEED_ONLY, dest: Path | None = None, ) -> Path | None: """Export the current database to an encrypted portable file.""" @@ -1233,7 +1201,6 @@ class PasswordManager: path = export_backup( self.vault, self.backup_manager, - mode, dest, parent_seed=self.parent_seed, ) @@ -1438,14 +1405,7 @@ class PasswordManager: # Create a new encryption manager with the new password mode = getattr(self, "encryption_mode", DEFAULT_ENCRYPTION_MODE) - try: - new_key = derive_index_key( - self.parent_seed, - new_password, - mode, - ) - except Exception: - new_key = derive_key_from_password(new_password) + new_key = derive_index_key(self.parent_seed) seed_key = derive_key_from_password(new_password) seed_mgr = EncryptionManager(seed_key, self.fingerprint_dir) @@ -1497,7 +1457,7 @@ class PasswordManager: index_data = self.vault.load_index() config_data = self.config_manager.load_config(require_pin=False) - new_key = derive_index_key(self.parent_seed, password, new_mode) + new_key = derive_index_key(self.parent_seed) new_mgr = EncryptionManager(new_key, self.fingerprint_dir) self.vault.set_encryption_manager(new_mgr) diff --git a/src/password_manager/portable_backup.py b/src/password_manager/portable_backup.py index 1c4a9eb..48ad688 100644 --- a/src/password_manager/portable_backup.py +++ b/src/password_manager/portable_backup.py @@ -20,7 +20,6 @@ from utils.key_derivation import ( EncryptionMode, DEFAULT_ENCRYPTION_MODE, ) -from utils.password_prompt import prompt_existing_password from password_manager.encryption import EncryptionManager from utils.checksum import json_checksum, canonical_json_dumps @@ -34,25 +33,17 @@ class PortableMode(Enum): """Encryption mode for portable exports.""" SEED_ONLY = EncryptionMode.SEED_ONLY.value - SEED_PLUS_PW = EncryptionMode.SEED_PLUS_PW.value - PW_ONLY = EncryptionMode.PW_ONLY.value -def _derive_export_key( - seed: str, - mode: PortableMode, - password: str | None = None, -) -> bytes: +def _derive_export_key(seed: str) -> bytes: """Derive the Fernet key for the export payload.""" - enc_mode = EncryptionMode(mode.value) - return derive_index_key(seed, password, enc_mode) + return derive_index_key(seed) def export_backup( vault: Vault, backup_manager: BackupManager, - mode: PortableMode = PortableMode.SEED_ONLY, dest_path: Path | None = None, *, publish: bool = False, @@ -72,11 +63,7 @@ def export_backup( if parent_seed is not None else vault.encryption_manager.decrypt_parent_seed() ) - password = None - if mode in (PortableMode.SEED_PLUS_PW, PortableMode.PW_ONLY): - password = prompt_existing_password("Enter your master password: ") - - key = _derive_export_key(seed, mode, password) + key = _derive_export_key(seed) enc_mgr = EncryptionManager(key, vault.fingerprint_dir) canonical = canonical_json_dumps(index_data) @@ -87,7 +74,7 @@ def export_backup( "format_version": FORMAT_VERSION, "created_at": int(time.time()), "fingerprint": vault.fingerprint_dir.name, - "encryption_mode": mode.value, + "encryption_mode": PortableMode.SEED_ONLY.value, "cipher": "fernet", "checksum": checksum, "payload": base64.b64encode(payload_bytes).decode("utf-8"), @@ -127,7 +114,8 @@ def import_backup( if wrapper.get("format_version") != FORMAT_VERSION: raise ValueError("Unsupported backup format") - mode = PortableMode(wrapper.get("encryption_mode", PortableMode.SEED_ONLY.value)) + if wrapper.get("encryption_mode") != PortableMode.SEED_ONLY.value: + raise ValueError("Unsupported encryption mode") payload = base64.b64decode(wrapper["payload"]) seed = ( @@ -135,11 +123,7 @@ def import_backup( if parent_seed is not None else vault.encryption_manager.decrypt_parent_seed() ) - password = None - if mode in (PortableMode.SEED_PLUS_PW, PortableMode.PW_ONLY): - password = prompt_existing_password("Enter your master password: ") - - key = _derive_export_key(seed, mode, password) + key = _derive_export_key(seed) enc_mgr = EncryptionManager(key, vault.fingerprint_dir) index_bytes = enc_mgr.decrypt_data(payload) index = json.loads(index_bytes.decode("utf-8")) diff --git a/src/tests/helpers.py b/src/tests/helpers.py index b6d61ec..914968a 100644 --- a/src/tests/helpers.py +++ b/src/tests/helpers.py @@ -8,7 +8,6 @@ from password_manager.encryption import EncryptionManager from utils.key_derivation import ( derive_index_key, derive_key_from_password, - EncryptionMode, ) TEST_SEED = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" @@ -19,14 +18,13 @@ def create_vault( dir_path: Path, seed: str = TEST_SEED, password: str = TEST_PASSWORD, - mode: EncryptionMode = EncryptionMode.SEED_ONLY, ) -> tuple[Vault, EncryptionManager]: """Create a Vault initialized for tests.""" seed_key = derive_key_from_password(password) seed_mgr = EncryptionManager(seed_key, dir_path) seed_mgr.encrypt_parent_seed(seed) - index_key = derive_index_key(seed, password, mode) + index_key = derive_index_key(seed) enc_mgr = EncryptionManager(index_key, dir_path) vault = Vault(enc_mgr, dir_path) return vault, enc_mgr diff --git a/src/tests/test_cli_encryption_mode.py b/src/tests/test_cli_encryption_mode.py index 7a54809..d5f235f 100644 --- a/src/tests/test_cli_encryption_mode.py +++ b/src/tests/test_cli_encryption_mode.py @@ -33,23 +33,3 @@ def _get_mode(monkeypatch, args=None, cfg=None): def test_default_mode_is_seed_only(monkeypatch): mode = _get_mode(monkeypatch) assert mode is EncryptionMode.SEED_ONLY - - -def test_cli_flag_overrides_config(monkeypatch): - cfg = {"encryption_mode": EncryptionMode.PW_ONLY.value} - mode = _get_mode(monkeypatch, ["--encryption-mode", "seed+pw"], cfg) - assert mode is EncryptionMode.SEED_PLUS_PW - - -def test_pw_only_emits_warning(monkeypatch, capsys): - pm = PasswordManager.__new__(PasswordManager) - pm.encryption_mode = EncryptionMode.SEED_ONLY - pm.fingerprint_manager = object() - pm.setup_existing_seed = lambda: None - pm.generate_new_seed = lambda: None - inputs = iter(["3", "1"]) - monkeypatch.setattr("builtins.input", lambda *_: next(inputs)) - pm.handle_new_seed_setup() - out = capsys.readouterr().out - assert "Password-only encryption is less secure" in out - assert pm.encryption_mode is EncryptionMode.PW_ONLY diff --git a/src/tests/test_cli_portable_backup_commands.py b/src/tests/test_cli_portable_backup_commands.py index fa6c86c..3b13dfb 100644 --- a/src/tests/test_cli_portable_backup_commands.py +++ b/src/tests/test_cli_portable_backup_commands.py @@ -7,7 +7,6 @@ import pytest sys.path.append(str(Path(__file__).resolve().parents[1])) import main -from password_manager.portable_backup import PortableMode from password_manager.manager import PasswordManager @@ -19,8 +18,8 @@ def _run(argv, monkeypatch): def fake_init(self, encryption_mode): called["init"] = True - def fake_export(self, mode, dest): - called["export"] = (mode, Path(dest)) + def fake_export(self, dest): + called["export"] = Path(dest) def fake_import(self, src): called["import"] = Path(src) @@ -36,8 +35,8 @@ def _run(argv, monkeypatch): def test_export_command_invokes_handler(monkeypatch): - called = _run(["export", "--mode", "pw-only", "--file", "out.json"], monkeypatch) - assert called["export"] == (PortableMode.PW_ONLY, Path("out.json")) + called = _run(["export", "--file", "out.json"], monkeypatch) + assert called["export"] == Path("out.json") assert "import" not in called diff --git a/src/tests/test_concurrency_stress.py b/src/tests/test_concurrency_stress.py index 0e16893..109e337 100644 --- a/src/tests/test_concurrency_stress.py +++ b/src/tests/test_concurrency_stress.py @@ -9,11 +9,7 @@ sys.path.append(str(Path(__file__).resolve().parents[1])) from password_manager.encryption import EncryptionManager from password_manager.vault import Vault from password_manager.backup import BackupManager -from utils.key_derivation import ( - derive_index_key, - derive_key_from_password, - EncryptionMode, -) +from utils.key_derivation import derive_index_key, derive_key_from_password def _writer(index_key: bytes, dir_path: Path, loops: int, out: Queue) -> None: @@ -50,7 +46,7 @@ def _backup(dir_path: Path, loops: int, out: Queue) -> None: @pytest.mark.parametrize("loops", [5, pytest.param(20, marks=pytest.mark.stress)]) @pytest.mark.parametrize("_", range(3)) def test_concurrency_stress(tmp_path: Path, loops: int, _): - index_key = derive_index_key(TEST_SEED, TEST_PASSWORD, EncryptionMode.SEED_ONLY) + index_key = derive_index_key(TEST_SEED) seed_key = derive_key_from_password(TEST_PASSWORD) EncryptionManager(seed_key, tmp_path).encrypt_parent_seed(TEST_SEED) enc = EncryptionManager(index_key, tmp_path) diff --git a/src/tests/test_encryption_mode_change.py b/src/tests/test_encryption_mode_change.py deleted file mode 100644 index e91ec0a..0000000 --- a/src/tests/test_encryption_mode_change.py +++ /dev/null @@ -1,57 +0,0 @@ -import sys -from pathlib import Path -from tempfile import TemporaryDirectory -from types import SimpleNamespace -from unittest.mock import patch, AsyncMock - -from helpers import create_vault, TEST_SEED, TEST_PASSWORD - -sys.path.append(str(Path(__file__).resolve().parents[1])) - -from password_manager.entry_management import EntryManager -from password_manager.config_manager import ConfigManager -from password_manager.vault import Vault -from password_manager.manager import PasswordManager -from utils.key_derivation import EncryptionMode - - -def test_change_encryption_mode(monkeypatch): - with TemporaryDirectory() as tmpdir: - fp = Path(tmpdir) - vault, enc_mgr = create_vault( - fp, TEST_SEED, TEST_PASSWORD, EncryptionMode.SEED_ONLY - ) - entry_mgr = EntryManager(vault, fp) - cfg_mgr = ConfigManager(vault, fp) - vault.save_index({"passwords": {}}) - - pm = PasswordManager.__new__(PasswordManager) - pm.encryption_manager = enc_mgr - pm.entry_manager = entry_mgr - pm.config_manager = cfg_mgr - pm.vault = vault - pm.password_generator = SimpleNamespace(encryption_manager=enc_mgr) - pm.fingerprint_dir = fp - pm.current_fingerprint = "fp" - pm.parent_seed = TEST_SEED - pm.encryption_mode = EncryptionMode.SEED_ONLY - - monkeypatch.setattr( - "password_manager.manager.prompt_existing_password", - lambda *_: TEST_PASSWORD, - ) - pm.verify_password = lambda pw: True - - with patch("password_manager.manager.NostrClient") as MockClient: - mock = MockClient.return_value - mock.publish_snapshot = AsyncMock(return_value=None) - pm.nostr_client = mock - pm.change_encryption_mode(EncryptionMode.SEED_PLUS_PW) - mock.publish_snapshot.assert_called_once() - - assert pm.encryption_mode is EncryptionMode.SEED_PLUS_PW - assert pm.password_generator.encryption_manager is pm.encryption_manager - loaded = vault.load_index() - assert loaded["passwords"] == {} - cfg = cfg_mgr.load_config(require_pin=False) - assert cfg["encryption_mode"] == EncryptionMode.SEED_PLUS_PW.value diff --git a/src/tests/test_encryption_mode_migration.py b/src/tests/test_encryption_mode_migration.py deleted file mode 100644 index 1970585..0000000 --- a/src/tests/test_encryption_mode_migration.py +++ /dev/null @@ -1,92 +0,0 @@ -import sys -from pathlib import Path -from tempfile import TemporaryDirectory -from types import SimpleNamespace - -import bcrypt -import pytest - -from helpers import create_vault, TEST_SEED, TEST_PASSWORD - -sys.path.append(str(Path(__file__).resolve().parents[1])) - -from password_manager.entry_management import EntryManager -from password_manager.config_manager import ConfigManager -from password_manager.vault import Vault -from password_manager.manager import PasswordManager -from utils.key_derivation import EncryptionMode - - -TRANSITIONS = [ - (EncryptionMode.SEED_ONLY, EncryptionMode.SEED_PLUS_PW), - (EncryptionMode.SEED_ONLY, EncryptionMode.PW_ONLY), - (EncryptionMode.SEED_PLUS_PW, EncryptionMode.SEED_ONLY), - (EncryptionMode.SEED_PLUS_PW, EncryptionMode.PW_ONLY), - (EncryptionMode.PW_ONLY, EncryptionMode.SEED_ONLY), - (EncryptionMode.PW_ONLY, EncryptionMode.SEED_PLUS_PW), -] - - -@pytest.mark.parametrize("start_mode,new_mode", TRANSITIONS) -def test_encryption_mode_migration(monkeypatch, start_mode, new_mode): - with TemporaryDirectory() as tmpdir: - fp = Path(tmpdir) - vault, enc_mgr = create_vault(fp, TEST_SEED, TEST_PASSWORD, start_mode) - entry_mgr = EntryManager(vault, fp) - cfg_mgr = ConfigManager(vault, fp) - - vault.save_index({"passwords": {}}) - cfg_mgr.save_config( - { - "relays": [], - "pin_hash": "", - "password_hash": bcrypt.hashpw( - TEST_PASSWORD.encode(), bcrypt.gensalt() - ).decode(), - "encryption_mode": start_mode.value, - } - ) - - pm = PasswordManager.__new__(PasswordManager) - pm.encryption_manager = enc_mgr - pm.entry_manager = entry_mgr - pm.config_manager = cfg_mgr - pm.vault = vault - pm.password_generator = SimpleNamespace(encryption_manager=enc_mgr) - pm.fingerprint_dir = fp - pm.current_fingerprint = "fp" - pm.parent_seed = TEST_SEED - pm.encryption_mode = start_mode - pm.nostr_client = SimpleNamespace(publish_snapshot=lambda *a, **k: None) - - monkeypatch.setattr( - "password_manager.manager.prompt_existing_password", - lambda *_: TEST_PASSWORD, - ) - monkeypatch.setattr( - "password_manager.manager.NostrClient", - lambda *a, **kw: SimpleNamespace(publish_snapshot=lambda *a, **k: None), - ) - - pm.change_encryption_mode(new_mode) - - assert pm.encryption_mode is new_mode - cfg = cfg_mgr.load_config(require_pin=False) - assert cfg["encryption_mode"] == new_mode.value - - pm.lock_vault() - - monkeypatch.setattr( - "password_manager.manager.prompt_existing_password", - lambda *_: TEST_PASSWORD, - ) - monkeypatch.setattr(PasswordManager, "initialize_bip85", lambda self: None) - monkeypatch.setattr(PasswordManager, "initialize_managers", lambda self: None) - - pm.unlock_vault() - - assert pm.parent_seed == TEST_SEED - assert not pm.locked - assert pm.encryption_mode is new_mode - assert pm.vault.load_index()["passwords"] == {} - assert pm.verify_password(TEST_PASSWORD) diff --git a/src/tests/test_index_import_export.py b/src/tests/test_index_import_export.py index b9d7fbb..87ec285 100644 --- a/src/tests/test_index_import_export.py +++ b/src/tests/test_index_import_export.py @@ -9,38 +9,26 @@ sys.path.append(str(Path(__file__).resolve().parents[1])) from password_manager.encryption import EncryptionManager from password_manager.vault import Vault -from utils.key_derivation import ( - derive_index_key, - derive_key_from_password, - EncryptionMode, -) +from utils.key_derivation import derive_index_key, derive_key_from_password SEED = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" PASSWORD = "passw0rd" -def setup_vault(tmp: Path, mode: EncryptionMode) -> Vault: +def setup_vault(tmp: Path) -> Vault: seed_key = derive_key_from_password(PASSWORD) seed_mgr = EncryptionManager(seed_key, tmp) seed_mgr.encrypt_parent_seed(SEED) - key = derive_index_key(SEED, PASSWORD, mode) + key = derive_index_key(SEED) enc_mgr = EncryptionManager(key, tmp) return Vault(enc_mgr, tmp) -@pytest.mark.parametrize( - "mode", - [ - EncryptionMode.SEED_ONLY, - EncryptionMode.SEED_PLUS_PW, - EncryptionMode.PW_ONLY, - ], -) -def test_index_export_import_round_trip(mode): +def test_index_export_import_round_trip(): with TemporaryDirectory() as td: tmp = Path(td) - vault = setup_vault(tmp, mode) + vault = setup_vault(tmp) original = {"passwords": {"0": {"website": "example"}}} vault.save_index(original) diff --git a/src/tests/test_key_derivation.py b/src/tests/test_key_derivation.py index 06b5f6a..a1ea90f 100644 --- a/src/tests/test_key_derivation.py +++ b/src/tests/test_key_derivation.py @@ -3,9 +3,7 @@ import pytest from utils.key_derivation import ( derive_key_from_password, derive_index_key_seed_only, - derive_index_key_seed_plus_pw, derive_index_key, - EncryptionMode, ) @@ -32,23 +30,6 @@ def test_seed_only_key_deterministic(): assert len(k1) == 44 -def test_seed_plus_pw_differs_from_seed_only(): +def test_derive_index_key_seed_only(): seed = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" - pw = "hunter2" - k1 = derive_index_key_seed_only(seed) - k2 = derive_index_key_seed_plus_pw(seed, pw) - assert k1 != k2 - - -def test_derive_index_key_modes(): - seed = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" - pw = "hunter2" - assert derive_index_key( - seed, pw, EncryptionMode.SEED_ONLY - ) == derive_index_key_seed_only(seed) - assert derive_index_key( - seed, pw, EncryptionMode.SEED_PLUS_PW - ) == derive_index_key_seed_plus_pw(seed, pw) - assert derive_index_key( - seed, pw, EncryptionMode.PW_ONLY - ) == derive_key_from_password(pw) + assert derive_index_key(seed) == derive_index_key_seed_only(seed) diff --git a/src/tests/test_password_change.py b/src/tests/test_password_change.py index 85ee9a6..8a4e4ea 100644 --- a/src/tests/test_password_change.py +++ b/src/tests/test_password_change.py @@ -29,7 +29,7 @@ def test_change_password_triggers_nostr_backup(monkeypatch): pm.password_generator = SimpleNamespace(encryption_manager=enc_mgr) pm.fingerprint_dir = fp pm.current_fingerprint = "fp" - pm.parent_seed = "seed" + pm.parent_seed = TEST_SEED pm.store_hashed_password = lambda pw: None pm.verify_password = lambda pw: True diff --git a/src/tests/test_password_unlock_after_change.py b/src/tests/test_password_unlock_after_change.py index 17e21b5..38e11e2 100644 --- a/src/tests/test_password_unlock_after_change.py +++ b/src/tests/test_password_unlock_after_change.py @@ -24,7 +24,7 @@ def test_password_change_and_unlock(monkeypatch): new_pw = "newpw" # initial encryption setup - index_key = derive_index_key(SEED, old_pw, EncryptionMode.SEED_PLUS_PW) + index_key = derive_index_key(SEED) seed_key = derive_key_from_password(old_pw) enc_mgr = EncryptionManager(index_key, fp) seed_mgr = EncryptionManager(seed_key, fp) @@ -45,7 +45,7 @@ def test_password_change_and_unlock(monkeypatch): seed_mgr.encrypt_parent_seed(SEED) pm = PasswordManager.__new__(PasswordManager) - pm.encryption_mode = EncryptionMode.SEED_PLUS_PW + pm.encryption_mode = EncryptionMode.SEED_ONLY pm.encryption_manager = enc_mgr pm.entry_manager = entry_mgr pm.config_manager = cfg_mgr diff --git a/src/tests/test_portable_backup.py b/src/tests/test_portable_backup.py index 16d1e3c..674e841 100644 --- a/src/tests/test_portable_backup.py +++ b/src/tests/test_portable_backup.py @@ -11,57 +11,39 @@ sys.path.append(str(Path(__file__).resolve().parents[1])) from password_manager.encryption import EncryptionManager from password_manager.vault import Vault from password_manager.backup import BackupManager -from password_manager.portable_backup import ( - PortableMode, - export_backup, - import_backup, -) -from utils.key_derivation import ( - derive_index_key, - derive_key_from_password, - EncryptionMode, -) +from password_manager.portable_backup import export_backup, import_backup +from utils.key_derivation import derive_index_key, derive_key_from_password SEED = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" PASSWORD = "passw0rd" -def setup_vault(tmp: Path, mode: EncryptionMode = EncryptionMode.SEED_ONLY): +def setup_vault(tmp: Path): seed_key = derive_key_from_password(PASSWORD) seed_mgr = EncryptionManager(seed_key, tmp) seed_mgr.encrypt_parent_seed(SEED) - index_key = derive_index_key(SEED, PASSWORD, mode) + index_key = derive_index_key(SEED) enc_mgr = EncryptionManager(index_key, tmp) vault = Vault(enc_mgr, tmp) backup = BackupManager(tmp) return vault, backup -def test_round_trip_across_modes(monkeypatch): - for pmode in [ - PortableMode.SEED_ONLY, - PortableMode.SEED_PLUS_PW, - PortableMode.PW_ONLY, - ]: - with TemporaryDirectory() as td: - tmp = Path(td) - vault, backup = setup_vault(tmp) - data = {"pw": 1} - vault.save_index(data) +def test_round_trip(monkeypatch): + with TemporaryDirectory() as td: + tmp = Path(td) + vault, backup = setup_vault(tmp) + data = {"pw": 1} + vault.save_index(data) - monkeypatch.setattr( - "password_manager.portable_backup.prompt_existing_password", - lambda *_a, **_k: PASSWORD, - ) + path = export_backup(vault, backup, parent_seed=SEED) + assert path.exists() - path = export_backup(vault, backup, pmode, parent_seed=SEED) - assert path.exists() - - vault.save_index({"pw": 0}) - import_backup(vault, backup, path, parent_seed=SEED) - assert vault.load_index()["pw"] == data["pw"] + vault.save_index({"pw": 0}) + import_backup(vault, backup, path, parent_seed=SEED) + assert vault.load_index()["pw"] == data["pw"] from cryptography.fernet import InvalidToken @@ -73,11 +55,7 @@ def test_corruption_detection(monkeypatch): vault, backup = setup_vault(tmp) vault.save_index({"a": 1}) - monkeypatch.setattr( - "password_manager.portable_backup.prompt_existing_password", - lambda *_a, **_k: PASSWORD, - ) - path = export_backup(vault, backup, PortableMode.SEED_ONLY, parent_seed=SEED) + path = export_backup(vault, backup, parent_seed=SEED) content = json.loads(path.read_text()) payload = base64.b64decode(content["payload"]) @@ -89,42 +67,13 @@ def test_corruption_detection(monkeypatch): import_backup(vault, backup, path, parent_seed=SEED) -def test_incorrect_credentials(monkeypatch): - with TemporaryDirectory() as td: - tmp = Path(td) - vault, backup = setup_vault(tmp) - vault.save_index({"a": 2}) - - monkeypatch.setattr( - "password_manager.portable_backup.prompt_existing_password", - lambda *_a, **_k: PASSWORD, - ) - path = export_backup( - vault, - backup, - PortableMode.SEED_PLUS_PW, - parent_seed=SEED, - ) - - monkeypatch.setattr( - "password_manager.portable_backup.prompt_existing_password", - lambda *_a, **_k: "wrong", - ) - with pytest.raises(Exception): - import_backup(vault, backup, path, parent_seed=SEED) - - def test_import_over_existing(monkeypatch): with TemporaryDirectory() as td: tmp = Path(td) vault, backup = setup_vault(tmp) vault.save_index({"v": 1}) - monkeypatch.setattr( - "password_manager.portable_backup.prompt_existing_password", - lambda *_a, **_k: PASSWORD, - ) - path = export_backup(vault, backup, PortableMode.SEED_ONLY, parent_seed=SEED) + path = export_backup(vault, backup, parent_seed=SEED) vault.save_index({"v": 2}) import_backup(vault, backup, path, parent_seed=SEED) @@ -138,21 +87,11 @@ def test_checksum_mismatch_detection(monkeypatch): vault, backup = setup_vault(tmp) vault.save_index({"a": 1}) - monkeypatch.setattr( - "password_manager.portable_backup.prompt_existing_password", - lambda *_a, **_k: PASSWORD, - ) - - path = export_backup( - vault, - backup, - PortableMode.SEED_ONLY, - parent_seed=SEED, - ) + path = export_backup(vault, backup, parent_seed=SEED) wrapper = json.loads(path.read_text()) payload = base64.b64decode(wrapper["payload"]) - key = derive_index_key(SEED, PASSWORD, EncryptionMode.SEED_ONLY) + key = derive_index_key(SEED) enc_mgr = EncryptionManager(key, tmp) data = json.loads(enc_mgr.decrypt_data(payload).decode()) data["a"] = 2 @@ -165,23 +104,14 @@ def test_checksum_mismatch_detection(monkeypatch): import_backup(vault, backup, path, parent_seed=SEED) -@pytest.mark.parametrize( - "pmode", - [PortableMode.SEED_ONLY, PortableMode.SEED_PLUS_PW], -) -def test_export_import_seed_encrypted_with_different_key(monkeypatch, pmode): +def test_export_import_seed_encrypted_with_different_key(monkeypatch): """Ensure backup round trip works when seed is encrypted with another key.""" with TemporaryDirectory() as td: tmp = Path(td) vault, backup = setup_vault(tmp) vault.save_index({"v": 123}) - monkeypatch.setattr( - "password_manager.portable_backup.prompt_existing_password", - lambda *_a, **_k: PASSWORD, - ) - - path = export_backup(vault, backup, pmode, parent_seed=SEED) + path = export_backup(vault, backup, parent_seed=SEED) vault.save_index({"v": 0}) import_backup(vault, backup, path, parent_seed=SEED) assert vault.load_index()["v"] == 123 diff --git a/src/utils/key_derivation.py b/src/utils/key_derivation.py index 0f9d6ff..0733424 100644 --- a/src/utils/key_derivation.py +++ b/src/utils/key_derivation.py @@ -41,8 +41,6 @@ class EncryptionMode(Enum): """Supported key derivation modes for database encryption.""" SEED_ONLY = "seed-only" - SEED_PLUS_PW = "seed+pw" - PW_ONLY = "pw-only" DEFAULT_ENCRYPTION_MODE = EncryptionMode.SEED_ONLY @@ -193,35 +191,6 @@ def derive_index_key_seed_only(seed: str) -> bytes: return base64.urlsafe_b64encode(key) -def derive_index_key_seed_plus_pw(seed: str, password: str) -> bytes: - """Derive the index key from seed and password combined.""" - seed_bytes = Bip39SeedGenerator(seed).Generate() - pw_bytes = unicodedata.normalize("NFKD", password).encode("utf-8") - hkdf = HKDF( - algorithm=hashes.SHA256(), - length=32, - salt=None, - info=b"password-db", - backend=default_backend(), - ) - key = hkdf.derive(seed_bytes + b"|" + pw_bytes) - return base64.urlsafe_b64encode(key) - - -def derive_index_key( - seed: str, - password: Optional[str] = None, - mode: EncryptionMode = DEFAULT_ENCRYPTION_MODE, -) -> bytes: - """Derive the index encryption key based on the selected mode.""" - if mode == EncryptionMode.SEED_ONLY: - return derive_index_key_seed_only(seed) - if mode == EncryptionMode.SEED_PLUS_PW: - if password is None: - raise ValueError("Password required for seed+pw mode") - return derive_index_key_seed_plus_pw(seed, password) - if mode == EncryptionMode.PW_ONLY: - if password is None: - raise ValueError("Password required for pw-only mode") - return derive_key_from_password(password) - raise ValueError(f"Unsupported encryption mode: {mode}") +def derive_index_key(seed: str) -> bytes: + """Derive the index encryption key.""" + return derive_index_key_seed_only(seed)