Merge pull request #155 from PR0M3TH3AN/codex/remove-seed_plus_pw-and-pw_only-modes

Drop unused encryption modes
This commit is contained in:
thePR0M3TH3AN
2025-07-02 18:51:19 -04:00
committed by GitHub
15 changed files with 56 additions and 427 deletions

View File

@@ -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))

View File

@@ -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)

View File

@@ -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"))

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -11,52 +11,34 @@ 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,
]:
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, pmode, parent_seed=SEED)
path = export_backup(vault, backup, parent_seed=SEED)
assert path.exists()
vault.save_index({"pw": 0})
@@ -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

View File

@@ -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:
def derive_index_key(seed: str) -> bytes:
"""Derive the index encryption key."""
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}")