mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
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:
@@ -1154,6 +1154,7 @@ class PasswordManager:
|
|||||||
self.backup_manager,
|
self.backup_manager,
|
||||||
mode,
|
mode,
|
||||||
dest,
|
dest,
|
||||||
|
parent_seed=self.parent_seed,
|
||||||
)
|
)
|
||||||
print(colored(f"Database exported to '{path}'.", "green"))
|
print(colored(f"Database exported to '{path}'.", "green"))
|
||||||
return path
|
return path
|
||||||
@@ -1165,7 +1166,12 @@ class PasswordManager:
|
|||||||
def handle_import_database(self, src: Path) -> None:
|
def handle_import_database(self, src: Path) -> None:
|
||||||
"""Import a portable database file, replacing the current index."""
|
"""Import a portable database file, replacing the current index."""
|
||||||
try:
|
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"))
|
print(colored("Database imported successfully.", "green"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Failed to import database: {e}", exc_info=True)
|
logging.error(f"Failed to import database: {e}", exc_info=True)
|
||||||
|
@@ -55,6 +55,7 @@ def export_backup(
|
|||||||
dest_path: Path | None = None,
|
dest_path: Path | None = None,
|
||||||
*,
|
*,
|
||||||
publish: bool = False,
|
publish: bool = False,
|
||||||
|
parent_seed: str | None = None,
|
||||||
) -> Path:
|
) -> Path:
|
||||||
"""Export the current vault state to a portable encrypted file."""
|
"""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)
|
dest_path = dest_dir / EXPORT_NAME_TEMPLATE.format(ts=ts)
|
||||||
|
|
||||||
index_data = vault.load_index()
|
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
|
password = None
|
||||||
if mode in (PortableMode.SEED_PLUS_PW, PortableMode.PW_ONLY):
|
if mode in (PortableMode.SEED_PLUS_PW, PortableMode.PW_ONLY):
|
||||||
password = prompt_existing_password("Enter your master password: ")
|
password = prompt_existing_password("Enter your master password: ")
|
||||||
@@ -109,6 +114,7 @@ def import_backup(
|
|||||||
vault: Vault,
|
vault: Vault,
|
||||||
backup_manager: BackupManager,
|
backup_manager: BackupManager,
|
||||||
path: Path,
|
path: Path,
|
||||||
|
parent_seed: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Import a portable backup file and replace the current index."""
|
"""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))
|
mode = PortableMode(wrapper.get("encryption_mode", PortableMode.SEED_ONLY.value))
|
||||||
payload = base64.b64decode(wrapper["payload"])
|
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
|
password = None
|
||||||
if mode in (PortableMode.SEED_PLUS_PW, PortableMode.PW_ONLY):
|
if mode in (PortableMode.SEED_PLUS_PW, PortableMode.PW_ONLY):
|
||||||
password = prompt_existing_password("Enter your master password: ")
|
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.encryption import EncryptionManager
|
||||||
from password_manager.vault import Vault
|
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"
|
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, 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)
|
key = derive_index_key(SEED, PASSWORD, mode)
|
||||||
enc_mgr = EncryptionManager(key, tmp)
|
enc_mgr = EncryptionManager(key, tmp)
|
||||||
enc_mgr.encrypt_parent_seed(SEED)
|
|
||||||
return Vault(enc_mgr, tmp)
|
return Vault(enc_mgr, tmp)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -16,7 +16,11 @@ from password_manager.portable_backup import (
|
|||||||
export_backup,
|
export_backup,
|
||||||
import_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"
|
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):
|
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)
|
index_key = derive_index_key(SEED, PASSWORD, mode)
|
||||||
enc_mgr = EncryptionManager(index_key, tmp)
|
enc_mgr = EncryptionManager(index_key, tmp)
|
||||||
enc_mgr.encrypt_parent_seed(SEED)
|
|
||||||
vault = Vault(enc_mgr, tmp)
|
vault = Vault(enc_mgr, tmp)
|
||||||
backup = BackupManager(tmp)
|
backup = BackupManager(tmp)
|
||||||
return vault, backup
|
return vault, backup
|
||||||
@@ -49,11 +56,11 @@ def test_round_trip_across_modes(monkeypatch):
|
|||||||
lambda *_a, **_k: PASSWORD,
|
lambda *_a, **_k: PASSWORD,
|
||||||
)
|
)
|
||||||
|
|
||||||
path = export_backup(vault, backup, pmode)
|
path = export_backup(vault, backup, pmode, parent_seed=SEED)
|
||||||
assert path.exists()
|
assert path.exists()
|
||||||
|
|
||||||
vault.save_index({"pw": 0})
|
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"]
|
assert vault.load_index()["pw"] == data["pw"]
|
||||||
|
|
||||||
|
|
||||||
@@ -70,7 +77,7 @@ def test_corruption_detection(monkeypatch):
|
|||||||
"password_manager.portable_backup.prompt_existing_password",
|
"password_manager.portable_backup.prompt_existing_password",
|
||||||
lambda *_a, **_k: 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())
|
content = json.loads(path.read_text())
|
||||||
payload = base64.b64decode(content["payload"])
|
payload = base64.b64decode(content["payload"])
|
||||||
@@ -79,7 +86,7 @@ def test_corruption_detection(monkeypatch):
|
|||||||
path.write_text(json.dumps(content))
|
path.write_text(json.dumps(content))
|
||||||
|
|
||||||
with pytest.raises(InvalidToken):
|
with pytest.raises(InvalidToken):
|
||||||
import_backup(vault, backup, path)
|
import_backup(vault, backup, path, parent_seed=SEED)
|
||||||
|
|
||||||
|
|
||||||
def test_incorrect_credentials(monkeypatch):
|
def test_incorrect_credentials(monkeypatch):
|
||||||
@@ -92,14 +99,19 @@ def test_incorrect_credentials(monkeypatch):
|
|||||||
"password_manager.portable_backup.prompt_existing_password",
|
"password_manager.portable_backup.prompt_existing_password",
|
||||||
lambda *_a, **_k: 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(
|
monkeypatch.setattr(
|
||||||
"password_manager.portable_backup.prompt_existing_password",
|
"password_manager.portable_backup.prompt_existing_password",
|
||||||
lambda *_a, **_k: "wrong",
|
lambda *_a, **_k: "wrong",
|
||||||
)
|
)
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(Exception):
|
||||||
import_backup(vault, backup, path)
|
import_backup(vault, backup, path, parent_seed=SEED)
|
||||||
|
|
||||||
|
|
||||||
def test_import_over_existing(monkeypatch):
|
def test_import_over_existing(monkeypatch):
|
||||||
@@ -112,10 +124,10 @@ def test_import_over_existing(monkeypatch):
|
|||||||
"password_manager.portable_backup.prompt_existing_password",
|
"password_manager.portable_backup.prompt_existing_password",
|
||||||
lambda *_a, **_k: 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})
|
vault.save_index({"v": 2})
|
||||||
import_backup(vault, backup, path)
|
import_backup(vault, backup, path, parent_seed=SEED)
|
||||||
loaded = vault.load_index()
|
loaded = vault.load_index()
|
||||||
assert loaded["v"] == 1
|
assert loaded["v"] == 1
|
||||||
|
|
||||||
@@ -131,7 +143,12 @@ def test_checksum_mismatch_detection(monkeypatch):
|
|||||||
lambda *_a, **_k: PASSWORD,
|
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())
|
wrapper = json.loads(path.read_text())
|
||||||
payload = base64.b64decode(wrapper["payload"])
|
payload = base64.b64decode(wrapper["payload"])
|
||||||
@@ -145,4 +162,26 @@ def test_checksum_mismatch_detection(monkeypatch):
|
|||||||
path.write_text(json.dumps(wrapper))
|
path.write_text(json.dumps(wrapper))
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
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