mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-09 07:48:57 +00:00
Merge pull request #155 from PR0M3TH3AN/codex/remove-seed_plus_pw-and-pw_only-modes
Drop unused encryption modes
This commit is contained in:
@@ -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))
|
||||
|
@@ -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)
|
||||
|
@@ -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"))
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
@@ -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)
|
@@ -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)
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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}")
|
||||
|
Reference in New Issue
Block a user