Merge pull request #119 from PR0M3TH3AN/codex/implement-export/import-database-features

Add portable backup export/import CLI
This commit is contained in:
thePR0M3TH3AN
2025-07-01 21:35:17 -04:00
committed by GitHub
3 changed files with 115 additions and 3 deletions

View File

@@ -13,6 +13,7 @@ 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
@@ -457,8 +458,10 @@ def handle_settings(password_manager: PasswordManager) -> None:
print("3. Change password") print("3. Change password")
print("4. Verify Script Checksum") print("4. Verify Script Checksum")
print("5. Backup Parent Seed") print("5. Backup Parent Seed")
print("6. Lock Vault") print("6. Export database")
print("7. Back") print("7. Import database")
print("8. Lock Vault")
print("9. Back")
choice = input("Select an option: ").strip() choice = input("Select an option: ").strip()
if choice == "1": if choice == "1":
handle_profiles_menu(password_manager) handle_profiles_menu(password_manager)
@@ -471,10 +474,16 @@ def handle_settings(password_manager: PasswordManager) -> None:
elif choice == "5": elif choice == "5":
password_manager.handle_backup_reveal_parent_seed() password_manager.handle_backup_reveal_parent_seed()
elif choice == "6": elif choice == "6":
password_manager.handle_export_database()
elif choice == "7":
path = input("Enter path to backup file: ").strip()
if path:
password_manager.handle_import_database(Path(path))
elif choice == "8":
password_manager.lock_vault() password_manager.lock_vault()
print(colored("Vault locked. Please re-enter your password.", "yellow")) print(colored("Vault locked. Please re-enter your password.", "yellow"))
password_manager.unlock_vault() password_manager.unlock_vault()
elif choice == "7": elif choice == "9":
break break
else: else:
print(colored("Invalid choice.", "red")) print(colored("Invalid choice.", "red"))
@@ -565,11 +574,25 @@ if __name__ == "__main__":
# Load config from disk and parse command-line arguments # Load config from disk and parse command-line arguments
cfg = load_global_config() cfg = load_global_config()
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
sub = parser.add_subparsers(dest="command")
parser.add_argument( parser.add_argument(
"--encryption-mode", "--encryption-mode",
choices=[m.value for m in EncryptionMode], choices=[m.value for m in EncryptionMode],
help="Select encryption mode", help="Select encryption mode",
) )
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")
imp = sub.add_parser("import")
imp.add_argument("--file")
args = parser.parse_args() args = parser.parse_args()
mode_value = cfg.get("encryption_mode", EncryptionMode.SEED_ONLY.value) mode_value = cfg.get("encryption_mode", EncryptionMode.SEED_ONLY.value)
@@ -591,6 +614,14 @@ if __name__ == "__main__":
print(colored(f"Error: Failed to initialize PasswordManager: {e}", "red")) print(colored(f"Error: Failed to initialize PasswordManager: {e}", "red"))
sys.exit(1) sys.exit(1)
if args.command == "export":
mode = PortableMode(args.mode)
password_manager.handle_export_database(mode, Path(args.file))
sys.exit(0)
elif args.command == "import":
password_manager.handle_import_database(Path(args.file))
sys.exit(0)
# Register signal handlers for graceful shutdown # Register signal handlers for graceful shutdown
def signal_handler(sig, frame): def signal_handler(sig, frame):
""" """

View File

@@ -24,6 +24,11 @@ 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 (
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,
@@ -1137,6 +1142,35 @@ class PasswordManager:
logging.error(f"Failed to restore backup: {e}", exc_info=True) logging.error(f"Failed to restore backup: {e}", exc_info=True)
print(colored(f"Error: Failed to restore backup: {e}", "red")) print(colored(f"Error: Failed to restore backup: {e}", "red"))
def handle_export_database(
self,
mode: "PortableMode" = PortableMode.SEED_ONLY,
dest: Path | None = None,
) -> Path | None:
"""Export the current database to an encrypted portable file."""
try:
path = export_backup(
self.vault,
self.backup_manager,
mode,
dest,
)
print(colored(f"Database exported to '{path}'.", "green"))
return path
except Exception as e:
logging.error(f"Failed to export database: {e}", exc_info=True)
print(colored(f"Error: Failed to export database: {e}", "red"))
return None
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)
print(colored("Database imported successfully.", "green"))
except Exception as e:
logging.error(f"Failed to import database: {e}", exc_info=True)
print(colored(f"Error: Failed to import database: {e}", "red"))
def handle_backup_reveal_parent_seed(self) -> None: def handle_backup_reveal_parent_seed(self) -> None:
""" """
Handles the backup and reveal of the parent seed. Handles the backup and reveal of the parent seed.

View File

@@ -0,0 +1,47 @@
import sys
from pathlib import Path
import runpy
import pytest
sys.path.append(str(Path(__file__).resolve().parents[1]))
import main
from password_manager.portable_backup import PortableMode
from password_manager.manager import PasswordManager
def _run(argv, monkeypatch):
monkeypatch.setattr(sys, "argv", ["seedpass"] + argv)
monkeypatch.setattr(main, "load_global_config", lambda: {})
called = {}
def fake_init(self, encryption_mode):
called["init"] = True
def fake_export(self, mode, dest):
called["export"] = (mode, Path(dest))
def fake_import(self, src):
called["import"] = Path(src)
monkeypatch.setattr(PasswordManager, "__init__", fake_init)
monkeypatch.setattr(PasswordManager, "handle_export_database", fake_export)
monkeypatch.setattr(PasswordManager, "handle_import_database", fake_import)
with pytest.raises(SystemExit):
runpy.run_module("main", run_name="__main__")
return called
def test_export_command_invokes_handler(monkeypatch):
called = _run(["export", "--mode", "pw-only", "--file", "out.json"], monkeypatch)
assert called["export"] == (PortableMode.PW_ONLY, Path("out.json"))
assert "import" not in called
def test_import_command_invokes_handler(monkeypatch):
called = _run(["import", "--file", "backup.json"], monkeypatch)
assert called["import"] == Path("backup.json")
assert "export" not in called