Merge pull request #123 from PR0M3TH3AN/codex/update-backup-methods-to-support-parent_seed

Support parent seed parameter for portable backups
This commit is contained in:
thePR0M3TH3AN
2025-07-01 22:23:50 -04:00
committed by GitHub
4 changed files with 79 additions and 17 deletions

View File

@@ -1154,6 +1154,7 @@ class PasswordManager:
self.backup_manager,
mode,
dest,
parent_seed=self.parent_seed,
)
print(colored(f"Database exported to '{path}'.", "green"))
return path
@@ -1165,7 +1166,12 @@ class PasswordManager:
def handle_import_database(self, src: Path) -> None:
"""Import a portable database file, replacing the current index."""
try:
import_backup(self.vault, self.backup_manager, src)
import_backup(
self.vault,
self.backup_manager,
src,
parent_seed=self.parent_seed,
)
print(colored("Database imported successfully.", "green"))
except Exception as e:
logging.error(f"Failed to import database: {e}", exc_info=True)

View File

@@ -55,6 +55,7 @@ def export_backup(
dest_path: Path | None = None,
*,
publish: bool = False,
parent_seed: str | None = None,
) -> Path:
"""Export the current vault state to a portable encrypted file."""
@@ -65,7 +66,11 @@ def export_backup(
dest_path = dest_dir / EXPORT_NAME_TEMPLATE.format(ts=ts)
index_data = vault.load_index()
seed = vault.encryption_manager.decrypt_parent_seed()
seed = (
parent_seed
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: ")
@@ -109,6 +114,7 @@ def import_backup(
vault: Vault,
backup_manager: BackupManager,
path: Path,
parent_seed: str | None = None,
) -> None:
"""Import a portable backup file and replace the current index."""
@@ -123,7 +129,11 @@ def import_backup(
mode = PortableMode(wrapper.get("encryption_mode", PortableMode.SEED_ONLY.value))
payload = base64.b64decode(wrapper["payload"])
seed = vault.encryption_manager.decrypt_parent_seed()
seed = (
parent_seed
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: ")

View File

@@ -9,16 +9,23 @@ 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, EncryptionMode
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"
PASSWORD = "passw0rd"
def setup_vault(tmp: Path, mode: EncryptionMode) -> 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)
enc_mgr = EncryptionManager(key, tmp)
enc_mgr.encrypt_parent_seed(SEED)
return Vault(enc_mgr, tmp)

View File

@@ -16,7 +16,11 @@ from password_manager.portable_backup import (
export_backup,
import_backup,
)
from utils.key_derivation import derive_index_key, EncryptionMode
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"
@@ -24,9 +28,12 @@ PASSWORD = "passw0rd"
def setup_vault(tmp: Path, mode: EncryptionMode = EncryptionMode.SEED_ONLY):
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)
enc_mgr = EncryptionManager(index_key, tmp)
enc_mgr.encrypt_parent_seed(SEED)
vault = Vault(enc_mgr, tmp)
backup = BackupManager(tmp)
return vault, backup
@@ -49,11 +56,11 @@ def test_round_trip_across_modes(monkeypatch):
lambda *_a, **_k: PASSWORD,
)
path = export_backup(vault, backup, pmode)
path = export_backup(vault, backup, pmode, parent_seed=SEED)
assert path.exists()
vault.save_index({"pw": 0})
import_backup(vault, backup, path)
import_backup(vault, backup, path, parent_seed=SEED)
assert vault.load_index()["pw"] == data["pw"]
@@ -70,7 +77,7 @@ def test_corruption_detection(monkeypatch):
"password_manager.portable_backup.prompt_existing_password",
lambda *_a, **_k: PASSWORD,
)
path = export_backup(vault, backup, PortableMode.SEED_ONLY)
path = export_backup(vault, backup, PortableMode.SEED_ONLY, parent_seed=SEED)
content = json.loads(path.read_text())
payload = base64.b64decode(content["payload"])
@@ -79,7 +86,7 @@ def test_corruption_detection(monkeypatch):
path.write_text(json.dumps(content))
with pytest.raises(InvalidToken):
import_backup(vault, backup, path)
import_backup(vault, backup, path, parent_seed=SEED)
def test_incorrect_credentials(monkeypatch):
@@ -92,14 +99,19 @@ def test_incorrect_credentials(monkeypatch):
"password_manager.portable_backup.prompt_existing_password",
lambda *_a, **_k: PASSWORD,
)
path = export_backup(vault, backup, PortableMode.SEED_PLUS_PW)
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)
import_backup(vault, backup, path, parent_seed=SEED)
def test_import_over_existing(monkeypatch):
@@ -112,10 +124,10 @@ def test_import_over_existing(monkeypatch):
"password_manager.portable_backup.prompt_existing_password",
lambda *_a, **_k: PASSWORD,
)
path = export_backup(vault, backup, PortableMode.SEED_ONLY)
path = export_backup(vault, backup, PortableMode.SEED_ONLY, parent_seed=SEED)
vault.save_index({"v": 2})
import_backup(vault, backup, path)
import_backup(vault, backup, path, parent_seed=SEED)
loaded = vault.load_index()
assert loaded["v"] == 1
@@ -131,7 +143,12 @@ def test_checksum_mismatch_detection(monkeypatch):
lambda *_a, **_k: PASSWORD,
)
path = export_backup(vault, backup, PortableMode.SEED_ONLY)
path = export_backup(
vault,
backup,
PortableMode.SEED_ONLY,
parent_seed=SEED,
)
wrapper = json.loads(path.read_text())
payload = base64.b64decode(wrapper["payload"])
@@ -145,4 +162,26 @@ def test_checksum_mismatch_detection(monkeypatch):
path.write_text(json.dumps(wrapper))
with pytest.raises(ValueError):
import_backup(vault, backup, path)
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):
"""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)
vault.save_index({"v": 0})
import_backup(vault, backup, path, parent_seed=SEED)
assert vault.load_index()["v"] == 123