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
|
import traceback
|
||||||
|
|
||||||
from password_manager.manager import PasswordManager
|
from password_manager.manager import PasswordManager
|
||||||
from password_manager.portable_backup import PortableMode
|
|
||||||
from nostr.client import NostrClient
|
from nostr.client import NostrClient
|
||||||
from constants import INACTIVITY_TIMEOUT
|
from constants import INACTIVITY_TIMEOUT
|
||||||
from utils.key_derivation import EncryptionMode
|
from utils.key_derivation import EncryptionMode
|
||||||
@@ -630,11 +629,6 @@ if __name__ == "__main__":
|
|||||||
)
|
)
|
||||||
|
|
||||||
exp = sub.add_parser("export")
|
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")
|
exp.add_argument("--file")
|
||||||
|
|
||||||
imp = sub.add_parser("import")
|
imp = sub.add_parser("import")
|
||||||
@@ -662,8 +656,7 @@ if __name__ == "__main__":
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if args.command == "export":
|
if args.command == "export":
|
||||||
mode = PortableMode(args.mode)
|
password_manager.handle_export_database(Path(args.file))
|
||||||
password_manager.handle_export_database(mode, Path(args.file))
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
elif args.command == "import":
|
elif args.command == "import":
|
||||||
password_manager.handle_import_database(Path(args.file))
|
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.password_generation import PasswordGenerator
|
||||||
from password_manager.backup import BackupManager
|
from password_manager.backup import BackupManager
|
||||||
from password_manager.vault import Vault
|
from password_manager.vault import Vault
|
||||||
from password_manager.portable_backup import (
|
from password_manager.portable_backup import export_backup, import_backup
|
||||||
export_backup,
|
|
||||||
import_backup,
|
|
||||||
PortableMode,
|
|
||||||
)
|
|
||||||
from utils.key_derivation import (
|
from utils.key_derivation import (
|
||||||
derive_key_from_parent_seed,
|
derive_key_from_parent_seed,
|
||||||
derive_key_from_password,
|
derive_key_from_password,
|
||||||
@@ -125,22 +121,7 @@ class PasswordManager:
|
|||||||
Returns:
|
Returns:
|
||||||
EncryptionMode: The chosen encryption mode.
|
EncryptionMode: The chosen encryption mode.
|
||||||
"""
|
"""
|
||||||
print("Choose encryption mode [Enter for seed-only]")
|
# Only seed-only mode is supported
|
||||||
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
|
|
||||||
return EncryptionMode.SEED_ONLY
|
return EncryptionMode.SEED_ONLY
|
||||||
|
|
||||||
def lock_vault(self) -> None:
|
def lock_vault(self) -> None:
|
||||||
@@ -309,11 +290,7 @@ class PasswordManager:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
key = derive_index_key(
|
key = derive_index_key(self.parent_seed)
|
||||||
self.parent_seed,
|
|
||||||
password,
|
|
||||||
self.encryption_mode,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.encryption_manager = EncryptionManager(key, fingerprint_dir)
|
self.encryption_manager = EncryptionManager(key, fingerprint_dir)
|
||||||
self.vault = Vault(self.encryption_manager, fingerprint_dir)
|
self.vault = Vault(self.encryption_manager, fingerprint_dir)
|
||||||
@@ -566,11 +543,7 @@ class PasswordManager:
|
|||||||
|
|
||||||
# Initialize EncryptionManager with key and fingerprint_dir
|
# Initialize EncryptionManager with key and fingerprint_dir
|
||||||
password = prompt_for_password()
|
password = prompt_for_password()
|
||||||
index_key = derive_index_key(
|
index_key = derive_index_key(parent_seed)
|
||||||
parent_seed,
|
|
||||||
password,
|
|
||||||
self.encryption_mode,
|
|
||||||
)
|
|
||||||
seed_key = derive_key_from_password(password)
|
seed_key = derive_key_from_password(password)
|
||||||
|
|
||||||
self.encryption_manager = EncryptionManager(index_key, fingerprint_dir)
|
self.encryption_manager = EncryptionManager(index_key, fingerprint_dir)
|
||||||
@@ -707,11 +680,7 @@ class PasswordManager:
|
|||||||
# Prompt for password
|
# Prompt for password
|
||||||
password = prompt_for_password()
|
password = prompt_for_password()
|
||||||
|
|
||||||
index_key = derive_index_key(
|
index_key = derive_index_key(seed)
|
||||||
seed,
|
|
||||||
password,
|
|
||||||
self.encryption_mode,
|
|
||||||
)
|
|
||||||
seed_key = derive_key_from_password(password)
|
seed_key = derive_key_from_password(password)
|
||||||
|
|
||||||
self.encryption_manager = EncryptionManager(index_key, fingerprint_dir)
|
self.encryption_manager = EncryptionManager(index_key, fingerprint_dir)
|
||||||
@@ -1225,7 +1194,6 @@ class PasswordManager:
|
|||||||
|
|
||||||
def handle_export_database(
|
def handle_export_database(
|
||||||
self,
|
self,
|
||||||
mode: "PortableMode" = PortableMode.SEED_ONLY,
|
|
||||||
dest: Path | None = None,
|
dest: Path | None = None,
|
||||||
) -> Path | None:
|
) -> Path | None:
|
||||||
"""Export the current database to an encrypted portable file."""
|
"""Export the current database to an encrypted portable file."""
|
||||||
@@ -1233,7 +1201,6 @@ class PasswordManager:
|
|||||||
path = export_backup(
|
path = export_backup(
|
||||||
self.vault,
|
self.vault,
|
||||||
self.backup_manager,
|
self.backup_manager,
|
||||||
mode,
|
|
||||||
dest,
|
dest,
|
||||||
parent_seed=self.parent_seed,
|
parent_seed=self.parent_seed,
|
||||||
)
|
)
|
||||||
@@ -1438,14 +1405,7 @@ class PasswordManager:
|
|||||||
|
|
||||||
# Create a new encryption manager with the new password
|
# Create a new encryption manager with the new password
|
||||||
mode = getattr(self, "encryption_mode", DEFAULT_ENCRYPTION_MODE)
|
mode = getattr(self, "encryption_mode", DEFAULT_ENCRYPTION_MODE)
|
||||||
try:
|
new_key = derive_index_key(self.parent_seed)
|
||||||
new_key = derive_index_key(
|
|
||||||
self.parent_seed,
|
|
||||||
new_password,
|
|
||||||
mode,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
new_key = derive_key_from_password(new_password)
|
|
||||||
|
|
||||||
seed_key = derive_key_from_password(new_password)
|
seed_key = derive_key_from_password(new_password)
|
||||||
seed_mgr = EncryptionManager(seed_key, self.fingerprint_dir)
|
seed_mgr = EncryptionManager(seed_key, self.fingerprint_dir)
|
||||||
@@ -1497,7 +1457,7 @@ class PasswordManager:
|
|||||||
index_data = self.vault.load_index()
|
index_data = self.vault.load_index()
|
||||||
config_data = self.config_manager.load_config(require_pin=False)
|
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)
|
new_mgr = EncryptionManager(new_key, self.fingerprint_dir)
|
||||||
|
|
||||||
self.vault.set_encryption_manager(new_mgr)
|
self.vault.set_encryption_manager(new_mgr)
|
||||||
|
@@ -20,7 +20,6 @@ from utils.key_derivation import (
|
|||||||
EncryptionMode,
|
EncryptionMode,
|
||||||
DEFAULT_ENCRYPTION_MODE,
|
DEFAULT_ENCRYPTION_MODE,
|
||||||
)
|
)
|
||||||
from utils.password_prompt import prompt_existing_password
|
|
||||||
from password_manager.encryption import EncryptionManager
|
from password_manager.encryption import EncryptionManager
|
||||||
from utils.checksum import json_checksum, canonical_json_dumps
|
from utils.checksum import json_checksum, canonical_json_dumps
|
||||||
|
|
||||||
@@ -34,25 +33,17 @@ class PortableMode(Enum):
|
|||||||
"""Encryption mode for portable exports."""
|
"""Encryption mode for portable exports."""
|
||||||
|
|
||||||
SEED_ONLY = EncryptionMode.SEED_ONLY.value
|
SEED_ONLY = EncryptionMode.SEED_ONLY.value
|
||||||
SEED_PLUS_PW = EncryptionMode.SEED_PLUS_PW.value
|
|
||||||
PW_ONLY = EncryptionMode.PW_ONLY.value
|
|
||||||
|
|
||||||
|
|
||||||
def _derive_export_key(
|
def _derive_export_key(seed: str) -> bytes:
|
||||||
seed: str,
|
|
||||||
mode: PortableMode,
|
|
||||||
password: str | None = None,
|
|
||||||
) -> bytes:
|
|
||||||
"""Derive the Fernet key for the export payload."""
|
"""Derive the Fernet key for the export payload."""
|
||||||
|
|
||||||
enc_mode = EncryptionMode(mode.value)
|
return derive_index_key(seed)
|
||||||
return derive_index_key(seed, password, enc_mode)
|
|
||||||
|
|
||||||
|
|
||||||
def export_backup(
|
def export_backup(
|
||||||
vault: Vault,
|
vault: Vault,
|
||||||
backup_manager: BackupManager,
|
backup_manager: BackupManager,
|
||||||
mode: PortableMode = PortableMode.SEED_ONLY,
|
|
||||||
dest_path: Path | None = None,
|
dest_path: Path | None = None,
|
||||||
*,
|
*,
|
||||||
publish: bool = False,
|
publish: bool = False,
|
||||||
@@ -72,11 +63,7 @@ def export_backup(
|
|||||||
if parent_seed is not None
|
if parent_seed is not None
|
||||||
else vault.encryption_manager.decrypt_parent_seed()
|
else vault.encryption_manager.decrypt_parent_seed()
|
||||||
)
|
)
|
||||||
password = None
|
key = _derive_export_key(seed)
|
||||||
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)
|
|
||||||
enc_mgr = EncryptionManager(key, vault.fingerprint_dir)
|
enc_mgr = EncryptionManager(key, vault.fingerprint_dir)
|
||||||
|
|
||||||
canonical = canonical_json_dumps(index_data)
|
canonical = canonical_json_dumps(index_data)
|
||||||
@@ -87,7 +74,7 @@ def export_backup(
|
|||||||
"format_version": FORMAT_VERSION,
|
"format_version": FORMAT_VERSION,
|
||||||
"created_at": int(time.time()),
|
"created_at": int(time.time()),
|
||||||
"fingerprint": vault.fingerprint_dir.name,
|
"fingerprint": vault.fingerprint_dir.name,
|
||||||
"encryption_mode": mode.value,
|
"encryption_mode": PortableMode.SEED_ONLY.value,
|
||||||
"cipher": "fernet",
|
"cipher": "fernet",
|
||||||
"checksum": checksum,
|
"checksum": checksum,
|
||||||
"payload": base64.b64encode(payload_bytes).decode("utf-8"),
|
"payload": base64.b64encode(payload_bytes).decode("utf-8"),
|
||||||
@@ -127,7 +114,8 @@ def import_backup(
|
|||||||
if wrapper.get("format_version") != FORMAT_VERSION:
|
if wrapper.get("format_version") != FORMAT_VERSION:
|
||||||
raise ValueError("Unsupported backup format")
|
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"])
|
payload = base64.b64decode(wrapper["payload"])
|
||||||
|
|
||||||
seed = (
|
seed = (
|
||||||
@@ -135,11 +123,7 @@ def import_backup(
|
|||||||
if parent_seed is not None
|
if parent_seed is not None
|
||||||
else vault.encryption_manager.decrypt_parent_seed()
|
else vault.encryption_manager.decrypt_parent_seed()
|
||||||
)
|
)
|
||||||
password = None
|
key = _derive_export_key(seed)
|
||||||
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)
|
|
||||||
enc_mgr = EncryptionManager(key, vault.fingerprint_dir)
|
enc_mgr = EncryptionManager(key, vault.fingerprint_dir)
|
||||||
index_bytes = enc_mgr.decrypt_data(payload)
|
index_bytes = enc_mgr.decrypt_data(payload)
|
||||||
index = json.loads(index_bytes.decode("utf-8"))
|
index = json.loads(index_bytes.decode("utf-8"))
|
||||||
|
@@ -8,7 +8,6 @@ from password_manager.encryption import EncryptionManager
|
|||||||
from utils.key_derivation import (
|
from utils.key_derivation import (
|
||||||
derive_index_key,
|
derive_index_key,
|
||||||
derive_key_from_password,
|
derive_key_from_password,
|
||||||
EncryptionMode,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
TEST_SEED = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
TEST_SEED = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||||
@@ -19,14 +18,13 @@ def create_vault(
|
|||||||
dir_path: Path,
|
dir_path: Path,
|
||||||
seed: str = TEST_SEED,
|
seed: str = TEST_SEED,
|
||||||
password: str = TEST_PASSWORD,
|
password: str = TEST_PASSWORD,
|
||||||
mode: EncryptionMode = EncryptionMode.SEED_ONLY,
|
|
||||||
) -> tuple[Vault, EncryptionManager]:
|
) -> tuple[Vault, EncryptionManager]:
|
||||||
"""Create a Vault initialized for tests."""
|
"""Create a Vault initialized for tests."""
|
||||||
seed_key = derive_key_from_password(password)
|
seed_key = derive_key_from_password(password)
|
||||||
seed_mgr = EncryptionManager(seed_key, dir_path)
|
seed_mgr = EncryptionManager(seed_key, dir_path)
|
||||||
seed_mgr.encrypt_parent_seed(seed)
|
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)
|
enc_mgr = EncryptionManager(index_key, dir_path)
|
||||||
vault = Vault(enc_mgr, dir_path)
|
vault = Vault(enc_mgr, dir_path)
|
||||||
return vault, enc_mgr
|
return vault, enc_mgr
|
||||||
|
@@ -33,23 +33,3 @@ def _get_mode(monkeypatch, args=None, cfg=None):
|
|||||||
def test_default_mode_is_seed_only(monkeypatch):
|
def test_default_mode_is_seed_only(monkeypatch):
|
||||||
mode = _get_mode(monkeypatch)
|
mode = _get_mode(monkeypatch)
|
||||||
assert mode is EncryptionMode.SEED_ONLY
|
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]))
|
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
||||||
|
|
||||||
import main
|
import main
|
||||||
from password_manager.portable_backup import PortableMode
|
|
||||||
from password_manager.manager import PasswordManager
|
from password_manager.manager import PasswordManager
|
||||||
|
|
||||||
|
|
||||||
@@ -19,8 +18,8 @@ def _run(argv, monkeypatch):
|
|||||||
def fake_init(self, encryption_mode):
|
def fake_init(self, encryption_mode):
|
||||||
called["init"] = True
|
called["init"] = True
|
||||||
|
|
||||||
def fake_export(self, mode, dest):
|
def fake_export(self, dest):
|
||||||
called["export"] = (mode, Path(dest))
|
called["export"] = Path(dest)
|
||||||
|
|
||||||
def fake_import(self, src):
|
def fake_import(self, src):
|
||||||
called["import"] = Path(src)
|
called["import"] = Path(src)
|
||||||
@@ -36,8 +35,8 @@ def _run(argv, monkeypatch):
|
|||||||
|
|
||||||
|
|
||||||
def test_export_command_invokes_handler(monkeypatch):
|
def test_export_command_invokes_handler(monkeypatch):
|
||||||
called = _run(["export", "--mode", "pw-only", "--file", "out.json"], monkeypatch)
|
called = _run(["export", "--file", "out.json"], monkeypatch)
|
||||||
assert called["export"] == (PortableMode.PW_ONLY, Path("out.json"))
|
assert called["export"] == Path("out.json")
|
||||||
assert "import" not in called
|
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.encryption import EncryptionManager
|
||||||
from password_manager.vault import Vault
|
from password_manager.vault import Vault
|
||||||
from password_manager.backup import BackupManager
|
from password_manager.backup import BackupManager
|
||||||
from utils.key_derivation import (
|
from utils.key_derivation import derive_index_key, derive_key_from_password
|
||||||
derive_index_key,
|
|
||||||
derive_key_from_password,
|
|
||||||
EncryptionMode,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _writer(index_key: bytes, dir_path: Path, loops: int, out: Queue) -> None:
|
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("loops", [5, pytest.param(20, marks=pytest.mark.stress)])
|
||||||
@pytest.mark.parametrize("_", range(3))
|
@pytest.mark.parametrize("_", range(3))
|
||||||
def test_concurrency_stress(tmp_path: Path, loops: int, _):
|
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)
|
seed_key = derive_key_from_password(TEST_PASSWORD)
|
||||||
EncryptionManager(seed_key, tmp_path).encrypt_parent_seed(TEST_SEED)
|
EncryptionManager(seed_key, tmp_path).encrypt_parent_seed(TEST_SEED)
|
||||||
enc = EncryptionManager(index_key, tmp_path)
|
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.encryption import EncryptionManager
|
||||||
from password_manager.vault import Vault
|
from password_manager.vault import Vault
|
||||||
from utils.key_derivation import (
|
from utils.key_derivation import derive_index_key, derive_key_from_password
|
||||||
derive_index_key,
|
|
||||||
derive_key_from_password,
|
|
||||||
EncryptionMode,
|
|
||||||
)
|
|
||||||
|
|
||||||
SEED = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
SEED = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||||
PASSWORD = "passw0rd"
|
PASSWORD = "passw0rd"
|
||||||
|
|
||||||
|
|
||||||
def setup_vault(tmp: Path, mode: EncryptionMode) -> Vault:
|
def setup_vault(tmp: Path) -> Vault:
|
||||||
seed_key = derive_key_from_password(PASSWORD)
|
seed_key = derive_key_from_password(PASSWORD)
|
||||||
seed_mgr = EncryptionManager(seed_key, tmp)
|
seed_mgr = EncryptionManager(seed_key, tmp)
|
||||||
seed_mgr.encrypt_parent_seed(SEED)
|
seed_mgr.encrypt_parent_seed(SEED)
|
||||||
|
|
||||||
key = derive_index_key(SEED, PASSWORD, mode)
|
key = derive_index_key(SEED)
|
||||||
enc_mgr = EncryptionManager(key, tmp)
|
enc_mgr = EncryptionManager(key, tmp)
|
||||||
return Vault(enc_mgr, tmp)
|
return Vault(enc_mgr, tmp)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
def test_index_export_import_round_trip():
|
||||||
"mode",
|
|
||||||
[
|
|
||||||
EncryptionMode.SEED_ONLY,
|
|
||||||
EncryptionMode.SEED_PLUS_PW,
|
|
||||||
EncryptionMode.PW_ONLY,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_index_export_import_round_trip(mode):
|
|
||||||
with TemporaryDirectory() as td:
|
with TemporaryDirectory() as td:
|
||||||
tmp = Path(td)
|
tmp = Path(td)
|
||||||
vault = setup_vault(tmp, mode)
|
vault = setup_vault(tmp)
|
||||||
|
|
||||||
original = {"passwords": {"0": {"website": "example"}}}
|
original = {"passwords": {"0": {"website": "example"}}}
|
||||||
vault.save_index(original)
|
vault.save_index(original)
|
||||||
|
@@ -3,9 +3,7 @@ import pytest
|
|||||||
from utils.key_derivation import (
|
from utils.key_derivation import (
|
||||||
derive_key_from_password,
|
derive_key_from_password,
|
||||||
derive_index_key_seed_only,
|
derive_index_key_seed_only,
|
||||||
derive_index_key_seed_plus_pw,
|
|
||||||
derive_index_key,
|
derive_index_key,
|
||||||
EncryptionMode,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -32,23 +30,6 @@ def test_seed_only_key_deterministic():
|
|||||||
assert len(k1) == 44
|
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"
|
seed = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||||
pw = "hunter2"
|
assert derive_index_key(seed) == derive_index_key_seed_only(seed)
|
||||||
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)
|
|
||||||
|
@@ -29,7 +29,7 @@ def test_change_password_triggers_nostr_backup(monkeypatch):
|
|||||||
pm.password_generator = SimpleNamespace(encryption_manager=enc_mgr)
|
pm.password_generator = SimpleNamespace(encryption_manager=enc_mgr)
|
||||||
pm.fingerprint_dir = fp
|
pm.fingerprint_dir = fp
|
||||||
pm.current_fingerprint = "fp"
|
pm.current_fingerprint = "fp"
|
||||||
pm.parent_seed = "seed"
|
pm.parent_seed = TEST_SEED
|
||||||
pm.store_hashed_password = lambda pw: None
|
pm.store_hashed_password = lambda pw: None
|
||||||
pm.verify_password = lambda pw: True
|
pm.verify_password = lambda pw: True
|
||||||
|
|
||||||
|
@@ -24,7 +24,7 @@ def test_password_change_and_unlock(monkeypatch):
|
|||||||
new_pw = "newpw"
|
new_pw = "newpw"
|
||||||
|
|
||||||
# initial encryption setup
|
# 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)
|
seed_key = derive_key_from_password(old_pw)
|
||||||
enc_mgr = EncryptionManager(index_key, fp)
|
enc_mgr = EncryptionManager(index_key, fp)
|
||||||
seed_mgr = EncryptionManager(seed_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)
|
seed_mgr.encrypt_parent_seed(SEED)
|
||||||
|
|
||||||
pm = PasswordManager.__new__(PasswordManager)
|
pm = PasswordManager.__new__(PasswordManager)
|
||||||
pm.encryption_mode = EncryptionMode.SEED_PLUS_PW
|
pm.encryption_mode = EncryptionMode.SEED_ONLY
|
||||||
pm.encryption_manager = enc_mgr
|
pm.encryption_manager = enc_mgr
|
||||||
pm.entry_manager = entry_mgr
|
pm.entry_manager = entry_mgr
|
||||||
pm.config_manager = cfg_mgr
|
pm.config_manager = cfg_mgr
|
||||||
|
@@ -11,57 +11,39 @@ sys.path.append(str(Path(__file__).resolve().parents[1]))
|
|||||||
from password_manager.encryption import EncryptionManager
|
from password_manager.encryption import EncryptionManager
|
||||||
from password_manager.vault import Vault
|
from password_manager.vault import Vault
|
||||||
from password_manager.backup import BackupManager
|
from password_manager.backup import BackupManager
|
||||||
from password_manager.portable_backup import (
|
from password_manager.portable_backup import export_backup, import_backup
|
||||||
PortableMode,
|
from utils.key_derivation import derive_index_key, derive_key_from_password
|
||||||
export_backup,
|
|
||||||
import_backup,
|
|
||||||
)
|
|
||||||
from utils.key_derivation import (
|
|
||||||
derive_index_key,
|
|
||||||
derive_key_from_password,
|
|
||||||
EncryptionMode,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
SEED = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
SEED = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||||
PASSWORD = "passw0rd"
|
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_key = derive_key_from_password(PASSWORD)
|
||||||
seed_mgr = EncryptionManager(seed_key, tmp)
|
seed_mgr = EncryptionManager(seed_key, tmp)
|
||||||
seed_mgr.encrypt_parent_seed(SEED)
|
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)
|
enc_mgr = EncryptionManager(index_key, tmp)
|
||||||
vault = Vault(enc_mgr, tmp)
|
vault = Vault(enc_mgr, tmp)
|
||||||
backup = BackupManager(tmp)
|
backup = BackupManager(tmp)
|
||||||
return vault, backup
|
return vault, backup
|
||||||
|
|
||||||
|
|
||||||
def test_round_trip_across_modes(monkeypatch):
|
def test_round_trip(monkeypatch):
|
||||||
for pmode in [
|
with TemporaryDirectory() as td:
|
||||||
PortableMode.SEED_ONLY,
|
tmp = Path(td)
|
||||||
PortableMode.SEED_PLUS_PW,
|
vault, backup = setup_vault(tmp)
|
||||||
PortableMode.PW_ONLY,
|
data = {"pw": 1}
|
||||||
]:
|
vault.save_index(data)
|
||||||
with TemporaryDirectory() as td:
|
|
||||||
tmp = Path(td)
|
|
||||||
vault, backup = setup_vault(tmp)
|
|
||||||
data = {"pw": 1}
|
|
||||||
vault.save_index(data)
|
|
||||||
|
|
||||||
monkeypatch.setattr(
|
path = export_backup(vault, backup, parent_seed=SEED)
|
||||||
"password_manager.portable_backup.prompt_existing_password",
|
assert path.exists()
|
||||||
lambda *_a, **_k: PASSWORD,
|
|
||||||
)
|
|
||||||
|
|
||||||
path = export_backup(vault, backup, pmode, parent_seed=SEED)
|
vault.save_index({"pw": 0})
|
||||||
assert path.exists()
|
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
|
from cryptography.fernet import InvalidToken
|
||||||
@@ -73,11 +55,7 @@ def test_corruption_detection(monkeypatch):
|
|||||||
vault, backup = setup_vault(tmp)
|
vault, backup = setup_vault(tmp)
|
||||||
vault.save_index({"a": 1})
|
vault.save_index({"a": 1})
|
||||||
|
|
||||||
monkeypatch.setattr(
|
path = export_backup(vault, backup, parent_seed=SEED)
|
||||||
"password_manager.portable_backup.prompt_existing_password",
|
|
||||||
lambda *_a, **_k: PASSWORD,
|
|
||||||
)
|
|
||||||
path = export_backup(vault, backup, PortableMode.SEED_ONLY, parent_seed=SEED)
|
|
||||||
|
|
||||||
content = json.loads(path.read_text())
|
content = json.loads(path.read_text())
|
||||||
payload = base64.b64decode(content["payload"])
|
payload = base64.b64decode(content["payload"])
|
||||||
@@ -89,42 +67,13 @@ def test_corruption_detection(monkeypatch):
|
|||||||
import_backup(vault, backup, path, parent_seed=SEED)
|
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):
|
def test_import_over_existing(monkeypatch):
|
||||||
with TemporaryDirectory() as td:
|
with TemporaryDirectory() as td:
|
||||||
tmp = Path(td)
|
tmp = Path(td)
|
||||||
vault, backup = setup_vault(tmp)
|
vault, backup = setup_vault(tmp)
|
||||||
vault.save_index({"v": 1})
|
vault.save_index({"v": 1})
|
||||||
|
|
||||||
monkeypatch.setattr(
|
path = export_backup(vault, backup, parent_seed=SEED)
|
||||||
"password_manager.portable_backup.prompt_existing_password",
|
|
||||||
lambda *_a, **_k: PASSWORD,
|
|
||||||
)
|
|
||||||
path = export_backup(vault, backup, PortableMode.SEED_ONLY, parent_seed=SEED)
|
|
||||||
|
|
||||||
vault.save_index({"v": 2})
|
vault.save_index({"v": 2})
|
||||||
import_backup(vault, backup, path, parent_seed=SEED)
|
import_backup(vault, backup, path, parent_seed=SEED)
|
||||||
@@ -138,21 +87,11 @@ def test_checksum_mismatch_detection(monkeypatch):
|
|||||||
vault, backup = setup_vault(tmp)
|
vault, backup = setup_vault(tmp)
|
||||||
vault.save_index({"a": 1})
|
vault.save_index({"a": 1})
|
||||||
|
|
||||||
monkeypatch.setattr(
|
path = export_backup(vault, backup, parent_seed=SEED)
|
||||||
"password_manager.portable_backup.prompt_existing_password",
|
|
||||||
lambda *_a, **_k: PASSWORD,
|
|
||||||
)
|
|
||||||
|
|
||||||
path = export_backup(
|
|
||||||
vault,
|
|
||||||
backup,
|
|
||||||
PortableMode.SEED_ONLY,
|
|
||||||
parent_seed=SEED,
|
|
||||||
)
|
|
||||||
|
|
||||||
wrapper = json.loads(path.read_text())
|
wrapper = json.loads(path.read_text())
|
||||||
payload = base64.b64decode(wrapper["payload"])
|
payload = base64.b64decode(wrapper["payload"])
|
||||||
key = derive_index_key(SEED, PASSWORD, EncryptionMode.SEED_ONLY)
|
key = derive_index_key(SEED)
|
||||||
enc_mgr = EncryptionManager(key, tmp)
|
enc_mgr = EncryptionManager(key, tmp)
|
||||||
data = json.loads(enc_mgr.decrypt_data(payload).decode())
|
data = json.loads(enc_mgr.decrypt_data(payload).decode())
|
||||||
data["a"] = 2
|
data["a"] = 2
|
||||||
@@ -165,23 +104,14 @@ def test_checksum_mismatch_detection(monkeypatch):
|
|||||||
import_backup(vault, backup, path, parent_seed=SEED)
|
import_backup(vault, backup, path, parent_seed=SEED)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
def test_export_import_seed_encrypted_with_different_key(monkeypatch):
|
||||||
"pmode",
|
|
||||||
[PortableMode.SEED_ONLY, PortableMode.SEED_PLUS_PW],
|
|
||||||
)
|
|
||||||
def test_export_import_seed_encrypted_with_different_key(monkeypatch, pmode):
|
|
||||||
"""Ensure backup round trip works when seed is encrypted with another key."""
|
"""Ensure backup round trip works when seed is encrypted with another key."""
|
||||||
with TemporaryDirectory() as td:
|
with TemporaryDirectory() as td:
|
||||||
tmp = Path(td)
|
tmp = Path(td)
|
||||||
vault, backup = setup_vault(tmp)
|
vault, backup = setup_vault(tmp)
|
||||||
vault.save_index({"v": 123})
|
vault.save_index({"v": 123})
|
||||||
|
|
||||||
monkeypatch.setattr(
|
path = export_backup(vault, backup, parent_seed=SEED)
|
||||||
"password_manager.portable_backup.prompt_existing_password",
|
|
||||||
lambda *_a, **_k: PASSWORD,
|
|
||||||
)
|
|
||||||
|
|
||||||
path = export_backup(vault, backup, pmode, parent_seed=SEED)
|
|
||||||
vault.save_index({"v": 0})
|
vault.save_index({"v": 0})
|
||||||
import_backup(vault, backup, path, parent_seed=SEED)
|
import_backup(vault, backup, path, parent_seed=SEED)
|
||||||
assert vault.load_index()["v"] == 123
|
assert vault.load_index()["v"] == 123
|
||||||
|
@@ -41,8 +41,6 @@ class EncryptionMode(Enum):
|
|||||||
"""Supported key derivation modes for database encryption."""
|
"""Supported key derivation modes for database encryption."""
|
||||||
|
|
||||||
SEED_ONLY = "seed-only"
|
SEED_ONLY = "seed-only"
|
||||||
SEED_PLUS_PW = "seed+pw"
|
|
||||||
PW_ONLY = "pw-only"
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_ENCRYPTION_MODE = EncryptionMode.SEED_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)
|
return base64.urlsafe_b64encode(key)
|
||||||
|
|
||||||
|
|
||||||
def derive_index_key_seed_plus_pw(seed: str, password: str) -> bytes:
|
def derive_index_key(seed: str) -> bytes:
|
||||||
"""Derive the index key from seed and password combined."""
|
"""Derive the index encryption key."""
|
||||||
seed_bytes = Bip39SeedGenerator(seed).Generate()
|
return derive_index_key_seed_only(seed)
|
||||||
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}")
|
|
||||||
|
Reference in New Issue
Block a user