mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
Add optional parent_seed to portable backup
This commit is contained in:
@@ -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)
|
||||
|
@@ -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: ")
|
||||
|
@@ -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)
|
||||
|
||||
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user