Remove password-based encryption modes

This commit is contained in:
thePR0M3TH3AN
2025-07-02 18:48:15 -04:00
parent 01d55073b1
commit d2832db194
15 changed files with 56 additions and 427 deletions

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