mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-07 06:48:52 +00:00
Compare commits
2 Commits
6928b4ddbf
...
1b6b0ab5c5
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1b6b0ab5c5 | ||
![]() |
87999b1888 |
14
README.md
14
README.md
@@ -435,6 +435,16 @@ For a full list of commands see [docs/advanced_cli.md](docs/advanced_cli.md). Th
|
||||
```
|
||||
*(or `python src/main.py` when running directly from the repository)*
|
||||
|
||||
To restore a previously backed up index at launch, provide the backup path
|
||||
and fingerprint:
|
||||
|
||||
```bash
|
||||
seedpass --restore-backup /path/to/backup.json.enc --fingerprint <fp>
|
||||
```
|
||||
|
||||
Without the flag, the startup prompt offers a **Restore from backup** option
|
||||
before the vault is initialized.
|
||||
|
||||
2. **Follow the Prompts:**
|
||||
|
||||
- **Seed Profile Selection:** If you have existing seed profiles, you'll be prompted to select one or add a new one.
|
||||
@@ -620,6 +630,10 @@ initial setup. You must provide both your 12‑word master seed and the master
|
||||
password that encrypted the vault; without the correct password the retrieved
|
||||
data cannot be decrypted.
|
||||
|
||||
Alternatively, a local backup file can be loaded at startup. Launch the
|
||||
application with `--restore-backup <file> --fingerprint <fp>` or choose the
|
||||
**Restore from backup** option presented before the vault initializes.
|
||||
|
||||
1. Start SeedPass and choose option **4** when prompted to set up a seed.
|
||||
2. Paste your BIP‑85 seed phrase when asked.
|
||||
3. Enter the master password associated with that seed.
|
||||
|
41
src/main.py
41
src/main.py
@@ -19,7 +19,7 @@ from termcolor import colored
|
||||
from utils.color_scheme import color_text
|
||||
import importlib
|
||||
|
||||
from seedpass.core.manager import PasswordManager
|
||||
from seedpass.core.manager import PasswordManager, restore_backup_index
|
||||
from nostr.client import NostrClient
|
||||
from seedpass.core.entry_types import EntryType
|
||||
from seedpass.core.config_manager import ConfigManager
|
||||
@@ -1285,6 +1285,10 @@ def main(argv: list[str] | None = None, *, fingerprint: str | None = None) -> in
|
||||
load_global_config()
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--fingerprint")
|
||||
parser.add_argument(
|
||||
"--restore-backup",
|
||||
help="Restore index from backup file before starting",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-clipboard",
|
||||
action="store_true",
|
||||
@@ -1315,6 +1319,41 @@ def main(argv: list[str] | None = None, *, fingerprint: str | None = None) -> in
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.restore_backup:
|
||||
fp_target = args.fingerprint or fingerprint
|
||||
if fp_target is None:
|
||||
print(
|
||||
colored(
|
||||
"Error: --fingerprint is required when using --restore-backup.",
|
||||
"red",
|
||||
)
|
||||
)
|
||||
return 1
|
||||
try:
|
||||
restore_backup_index(Path(args.restore_backup), fp_target)
|
||||
logger.info("Restored backup from %s", args.restore_backup)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to restore backup: {e}", exc_info=True)
|
||||
print(colored(f"Error: Failed to restore backup: {e}", "red"))
|
||||
return 1
|
||||
elif args.command is None:
|
||||
print("Startup Options:")
|
||||
print("1. Continue")
|
||||
print("2. Restore from backup")
|
||||
choice = input("Select an option: ").strip()
|
||||
if choice == "2":
|
||||
path = input("Enter backup file path: ").strip()
|
||||
fp_target = args.fingerprint or fingerprint
|
||||
if fp_target is None:
|
||||
fp_target = input("Enter fingerprint for restore: ").strip()
|
||||
try:
|
||||
restore_backup_index(Path(path), fp_target)
|
||||
logger.info("Restored backup from %s", path)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to restore backup: {e}", exc_info=True)
|
||||
print(colored(f"Error: Failed to restore backup: {e}", "red"))
|
||||
return 1
|
||||
|
||||
if args.max_prompt_attempts is not None:
|
||||
os.environ["SEEDPASS_MAX_PROMPT_ATTEMPTS"] = str(args.max_prompt_attempts)
|
||||
|
||||
|
@@ -158,6 +158,31 @@ def calculate_profile_id(seed: str) -> str:
|
||||
return fp or ""
|
||||
|
||||
|
||||
def restore_backup_index(path: Path, fingerprint: str) -> None:
|
||||
"""Restore the encrypted index for ``fingerprint`` from ``path``.
|
||||
|
||||
This helper is intended for use before full :class:`PasswordManager`
|
||||
initialization. It simply copies the provided backup file into the
|
||||
profile's directory, replacing the existing index if present. A copy of
|
||||
the previous index is kept with a ``.bak`` suffix to allow manual
|
||||
recovery if needed.
|
||||
"""
|
||||
|
||||
fingerprint_dir = APP_DIR / fingerprint
|
||||
fingerprint_dir.mkdir(parents=True, exist_ok=True)
|
||||
dest = fingerprint_dir / "seedpass_entries_db.json.enc"
|
||||
src = Path(path)
|
||||
|
||||
# Ensure the source file can be read
|
||||
src.read_bytes()
|
||||
|
||||
if dest.exists():
|
||||
shutil.copy2(dest, dest.with_suffix(".bak"))
|
||||
|
||||
shutil.copy2(src, dest)
|
||||
os.chmod(dest, 0o600)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Notification:
|
||||
"""Simple message container for UI notifications."""
|
||||
|
56
src/tests/test_backup_restore_startup.py
Normal file
56
src/tests/test_backup_restore_startup.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import main
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_cli_flag_restores_before_init(monkeypatch, tmp_path):
|
||||
calls = []
|
||||
backup = tmp_path / "bak.json"
|
||||
backup.write_text("{}")
|
||||
|
||||
def fake_restore(path, fingerprint):
|
||||
calls.append(("restore", Path(path), fingerprint))
|
||||
|
||||
class DummyPM:
|
||||
def __init__(self, fingerprint=None):
|
||||
calls.append(("init", fingerprint))
|
||||
self.secret_mode_enabled = True
|
||||
self.inactivity_timeout = 0
|
||||
|
||||
monkeypatch.setattr(main, "restore_backup_index", fake_restore)
|
||||
monkeypatch.setattr(main, "PasswordManager", DummyPM)
|
||||
monkeypatch.setattr(main, "display_menu", lambda pm, **k: None)
|
||||
|
||||
rc = main.main(["--fingerprint", "fp", "--restore-backup", str(backup)])
|
||||
assert rc == 0
|
||||
assert calls[0][0] == "restore"
|
||||
assert calls[1][0] == "init"
|
||||
assert calls[0][1] == backup
|
||||
assert calls[0][2] == "fp"
|
||||
|
||||
|
||||
def test_menu_option_restores_before_init(monkeypatch, tmp_path):
|
||||
calls = []
|
||||
backup = tmp_path / "bak.json"
|
||||
backup.write_text("{}")
|
||||
|
||||
def fake_restore(path, fingerprint):
|
||||
calls.append(("restore", Path(path), fingerprint))
|
||||
|
||||
class DummyPM:
|
||||
def __init__(self, fingerprint=None):
|
||||
calls.append(("init", fingerprint))
|
||||
self.secret_mode_enabled = True
|
||||
self.inactivity_timeout = 0
|
||||
|
||||
monkeypatch.setattr(main, "restore_backup_index", fake_restore)
|
||||
monkeypatch.setattr(main, "PasswordManager", DummyPM)
|
||||
monkeypatch.setattr(main, "display_menu", lambda pm, **k: None)
|
||||
inputs = iter(["2", str(backup)])
|
||||
monkeypatch.setattr("builtins.input", lambda _prompt="": next(inputs))
|
||||
|
||||
rc = main.main(["--fingerprint", "fp"])
|
||||
assert rc == 0
|
||||
assert calls[0][0] == "restore"
|
||||
assert calls[1][0] == "init"
|
||||
assert calls[0][1] == backup
|
||||
assert calls[0][2] == "fp"
|
Reference in New Issue
Block a user