Files
seedPass/src/tests/test_portable_backup.py
2025-08-11 19:35:03 -04:00

166 lines
5.5 KiB
Python

import json
import base64
import time
from pathlib import Path
from tempfile import TemporaryDirectory
import pytest
import sys
sys.path.append(str(Path(__file__).resolve().parents[1]))
import seedpass.core.encryption as enc_module
from seedpass.core.encryption import EncryptionManager
from seedpass.core.vault import Vault
from seedpass.core.backup import BackupManager
from seedpass.core.config_manager import ConfigManager
from seedpass.core.portable_backup import export_backup, import_backup
from utils.key_derivation import derive_index_key, derive_key_from_password
from utils.fingerprint import generate_fingerprint
SEED = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
PASSWORD = "passw0rd"
def setup_vault(tmp: Path):
fp = generate_fingerprint(SEED)
seed_key = derive_key_from_password(PASSWORD, fp)
seed_mgr = EncryptionManager(seed_key, tmp)
seed_mgr.encrypt_parent_seed(SEED)
index_key = derive_index_key(SEED)
enc_mgr = EncryptionManager(index_key, tmp)
vault = Vault(enc_mgr, tmp)
cfg = ConfigManager(vault, tmp)
backup = BackupManager(tmp, cfg)
return vault, backup, cfg
def test_round_trip(monkeypatch):
with TemporaryDirectory() as td:
tmp = Path(td)
vault, backup, _ = setup_vault(tmp)
data = {"pw": 1}
vault.save_index(data)
path = export_backup(vault, backup, parent_seed=SEED)
assert path.exists()
wrapper = json.loads(path.read_text())
assert wrapper.get("cipher") == "aes-gcm"
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
@pytest.mark.skipif(sys.platform.startswith("win"), reason="flaky on Windows")
def test_corruption_detection(monkeypatch):
with TemporaryDirectory() as td:
tmp = Path(td)
vault, backup, _ = setup_vault(tmp)
vault.save_index({"a": 1})
path = export_backup(vault, backup, parent_seed=SEED)
content = json.loads(path.read_text())
payload = base64.b64decode(content["payload"])
payload = b"x" + payload[1:]
content["payload"] = base64.b64encode(payload).decode()
path.write_text(json.dumps(content))
def _fast_legacy_key(password: str, iterations: int = 100_000) -> bytes:
return base64.urlsafe_b64encode(b"0" * 32)
monkeypatch.setattr(
enc_module, "_derive_legacy_key_from_password", _fast_legacy_key
)
monkeypatch.setattr(
enc_module, "prompt_existing_password", lambda *_a, **_k: PASSWORD
)
with pytest.raises(InvalidToken):
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})
path = export_backup(vault, backup, parent_seed=SEED)
vault.save_index({"v": 2})
import_backup(vault, backup, path, parent_seed=SEED)
loaded = vault.load_index()
assert loaded["v"] == 1
def test_checksum_mismatch_detection(monkeypatch):
with TemporaryDirectory() as td:
tmp = Path(td)
vault, backup, _ = setup_vault(tmp)
vault.save_index({"a": 1})
path = export_backup(vault, backup, parent_seed=SEED)
wrapper = json.loads(path.read_text())
payload = base64.b64decode(wrapper["payload"])
key = derive_index_key(SEED)
enc_mgr = EncryptionManager(key, tmp)
data = json.loads(enc_mgr.decrypt_data(payload).decode())
data["a"] = 2
mod_canon = json.dumps(data, sort_keys=True, separators=(",", ":"))
new_payload = enc_mgr.encrypt_data(mod_canon.encode())
wrapper["payload"] = base64.b64encode(new_payload).decode()
path.write_text(json.dumps(wrapper))
with pytest.raises(ValueError):
import_backup(vault, backup, path, parent_seed=SEED)
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})
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
def test_export_creates_additional_backup_and_import(monkeypatch):
with TemporaryDirectory() as td, TemporaryDirectory() as extra:
tmp = Path(td)
fp = generate_fingerprint(SEED)
seed_key = derive_key_from_password(PASSWORD, fp)
seed_mgr = EncryptionManager(seed_key, tmp)
seed_mgr.encrypt_parent_seed(SEED)
index_key = derive_index_key(SEED)
enc_mgr = EncryptionManager(index_key, tmp)
vault = Vault(enc_mgr, tmp)
cfg = ConfigManager(vault, tmp)
cfg.set_additional_backup_path(extra)
backup = BackupManager(tmp, cfg)
vault.save_index({"v": 1})
monkeypatch.setattr(time, "time", lambda: 4444)
path = export_backup(vault, backup, parent_seed=SEED)
extra_file = Path(extra) / f"{tmp.name}_{path.name}"
assert extra_file.exists()
vault.save_index({"v": 0})
import_backup(vault, backup, extra_file, parent_seed=SEED)
assert vault.load_index()["v"] == 1