mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-09 15:58:48 +00:00
14
README.md
14
README.md
@@ -355,6 +355,20 @@ variable to control the delay between publishes when experimenting with large va
|
|||||||
pytest -vv -s -n 0 src/tests/test_nostr_index_size.py --desktop --max-entries=1000
|
pytest -vv -s -n 0 src/tests/test_nostr_index_size.py --desktop --max-entries=1000
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Generating a Test Profile
|
||||||
|
|
||||||
|
Use the helper script below to populate a profile with sample entries for testing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/generate_test_profile.py --profile demo_profile --count 100
|
||||||
|
```
|
||||||
|
|
||||||
|
The script now determines the fingerprint from the generated seed and stores the
|
||||||
|
vault under `~/.seedpass/<fingerprint>`. It also prints the fingerprint after
|
||||||
|
creation and publishes the encrypted index to Nostr. Use that same seed phrase
|
||||||
|
to load SeedPass. The app checks Nostr on startup and pulls any newer snapshot
|
||||||
|
so your vault stays in sync across machines.
|
||||||
|
|
||||||
### Automatically Updating the Script Checksum
|
### Automatically Updating the Script Checksum
|
||||||
|
|
||||||
SeedPass stores a SHA-256 checksum for the main program in `~/.seedpass/seedpass_script_checksum.txt`.
|
SeedPass stores a SHA-256 checksum for the main program in `~/.seedpass/seedpass_script_checksum.txt`.
|
||||||
|
@@ -1,7 +1,12 @@
|
|||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
layout: fixed
|
layout: fixed
|
||||||
theme: neo-dark
|
theme: base
|
||||||
|
themeVariables:
|
||||||
|
primaryColor: '#e94a39'
|
||||||
|
primaryBorderColor: '#e94a39'
|
||||||
|
primaryTextColor: '#ffffff'
|
||||||
|
lineColor: '#e94a39'
|
||||||
look: classic
|
look: classic
|
||||||
---
|
---
|
||||||
flowchart TD
|
flowchart TD
|
||||||
@@ -11,7 +16,7 @@ flowchart TD
|
|||||||
end
|
end
|
||||||
subgraph subGraph1["Backup Pipeline"]
|
subgraph subGraph1["Backup Pipeline"]
|
||||||
direction TB
|
direction TB
|
||||||
BK1["Incremental Backups<br>(BackupManager)"]
|
BK1["Timestamped Backups<br>(BackupManager)"]
|
||||||
BK2["Portable Backup<br>(portable_backup.py)<br>.json.enc"]
|
BK2["Portable Backup<br>(portable_backup.py)<br>.json.enc"]
|
||||||
BK3["Nostr Snapshot<br>(nostr.client)<br>gzip chunks"]
|
BK3["Nostr Snapshot<br>(nostr.client)<br>gzip chunks"]
|
||||||
end
|
end
|
||||||
|
@@ -49,9 +49,28 @@
|
|||||||
<!-- Intro Section -->
|
<!-- Intro Section -->
|
||||||
<section class="intro" id="intro" aria-labelledby="intro-heading">
|
<section class="intro" id="intro" aria-labelledby="intro-heading">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1 id="intro-heading">SeedPass: Secure Password Manager</h1>
|
<h1 id="intro-heading">One Seed to Rule Them All</h1>
|
||||||
<p><strong>SeedPass</strong> is a secure password generator and manager built on Bitcoin's BIP-85 standard. It uses deterministic key derivation to generate passwords that are never stored but can be easily regenerated when needed.</p>
|
<p>SeedPass deterministically derives every key and password from a single 12‑word phrase.</p>
|
||||||
<p>By integrating with the <strong>Nostr network</strong>, SeedPass compresses your encrypted vault and publishes it in 50 KB chunks. Each chunk is sent as a parameterised replaceable event, with deltas tracking changes between snapshots and automatic rotation when deltas grow large.</p>
|
<pre class="mermaid mini-chart">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
theme: base
|
||||||
|
themeVariables:
|
||||||
|
primaryColor: '#e94a39'
|
||||||
|
primaryBorderColor: '#e94a39'
|
||||||
|
lineColor: '#e94a39'
|
||||||
|
look: classic
|
||||||
|
---
|
||||||
|
flowchart TB
|
||||||
|
seed["alpha bravo charlie delta echo foxtrot golf hotel india juliet kilo lima"]
|
||||||
|
seed --> pw["🔑 Passwords"]
|
||||||
|
seed --> totp["📱 2FA Codes"]
|
||||||
|
seed --> ssh["🖧 SSH Keys"]
|
||||||
|
seed --> pgp["🔒 PGP Key"]
|
||||||
|
seed --> mn["🌱 Seed Phrase"]
|
||||||
|
seed --> nostr["⚡ Nostr Keys"]
|
||||||
|
classDef default fill:#ffffff,stroke:#e94a39,stroke-width:2px,color:#283c4f;
|
||||||
|
</pre>
|
||||||
<a href="https://github.com/PR0M3TH3AN/SeedPass" class="btn-primary cta-button"><i class="fas fa-download" aria-hidden="true"></i> Get Started</a>
|
<a href="https://github.com/PR0M3TH3AN/SeedPass" class="btn-primary cta-button"><i class="fas fa-download" aria-hidden="true"></i> Get Started</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -63,7 +82,12 @@
|
|||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
layout: fixed
|
layout: fixed
|
||||||
theme: neo-dark
|
theme: base
|
||||||
|
themeVariables:
|
||||||
|
primaryColor: '#e94a39'
|
||||||
|
primaryBorderColor: '#e94a39'
|
||||||
|
primaryTextColor: '#ffffff'
|
||||||
|
lineColor: '#e94a39'
|
||||||
look: classic
|
look: classic
|
||||||
---
|
---
|
||||||
flowchart TD
|
flowchart TD
|
||||||
@@ -73,7 +97,7 @@ flowchart TD
|
|||||||
end
|
end
|
||||||
subgraph subGraph1["Backup Pipeline"]
|
subgraph subGraph1["Backup Pipeline"]
|
||||||
direction TB
|
direction TB
|
||||||
BK1["Incremental Backups<br>(BackupManager)"]
|
BK1["Timestamped Backups<br>(BackupManager)"]
|
||||||
BK2["Portable Backup<br>(portable_backup.py)<br>.json.enc"]
|
BK2["Portable Backup<br>(portable_backup.py)<br>.json.enc"]
|
||||||
BK3["Nostr Snapshot<br>(nostr.client)<br>gzip chunks"]
|
BK3["Nostr Snapshot<br>(nostr.client)<br>gzip chunks"]
|
||||||
end
|
end
|
||||||
|
@@ -337,6 +337,13 @@ footer .social-media a:focus {
|
|||||||
transform: translateY(-3px);
|
transform: translateY(-3px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mini flow chart in hero */
|
||||||
|
.mini-chart {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 40px auto;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
/* Features Section */
|
/* Features Section */
|
||||||
.features {
|
.features {
|
||||||
background-color: var(--background-section);
|
background-color: var(--background-section);
|
||||||
|
199
scripts/generate_test_profile.py
Normal file
199
scripts/generate_test_profile.py
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate a SeedPass test profile with realistic entries.
|
||||||
|
|
||||||
|
This script populates a profile directory with a variety of entry types.
|
||||||
|
If the profile does not exist, a new BIP-39 seed phrase is generated and
|
||||||
|
stored encrypted. A clear text copy is written to ``seed_phrase.txt`` so
|
||||||
|
it can be reused across devices.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from bip_utils import Bip39Languages, Bip39MnemonicGenerator, Bip39WordsNum
|
||||||
|
|
||||||
|
# Ensure src directory is in sys.path for imports
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
SRC_DIR = PROJECT_ROOT / "src"
|
||||||
|
if str(SRC_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(SRC_DIR))
|
||||||
|
|
||||||
|
import constants as consts
|
||||||
|
|
||||||
|
# Use a dedicated subdirectory for test profiles so regular data is not polluted
|
||||||
|
consts.APP_DIR = consts.APP_DIR / "tests"
|
||||||
|
consts.PARENT_SEED_FILE = consts.APP_DIR / "parent_seed.enc"
|
||||||
|
consts.SCRIPT_CHECKSUM_FILE = consts.APP_DIR / "seedpass_script_checksum.txt"
|
||||||
|
|
||||||
|
from constants import APP_DIR, initialize_app
|
||||||
|
from utils.key_derivation import derive_key_from_password, derive_index_key
|
||||||
|
from password_manager.encryption import EncryptionManager
|
||||||
|
from password_manager.vault import Vault
|
||||||
|
from password_manager.config_manager import ConfigManager
|
||||||
|
from password_manager.backup import BackupManager
|
||||||
|
from password_manager.entry_management import EntryManager
|
||||||
|
from nostr.client import NostrClient
|
||||||
|
from utils.fingerprint import generate_fingerprint
|
||||||
|
from utils.fingerprint_manager import FingerprintManager
|
||||||
|
import bcrypt
|
||||||
|
import asyncio
|
||||||
|
import gzip
|
||||||
|
|
||||||
|
DEFAULT_PASSWORD = "testpassword"
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_profile(profile_name: str) -> tuple[str, EntryManager, Path, str]:
|
||||||
|
"""Create or load a profile and return the seed phrase, manager, directory and fingerprint."""
|
||||||
|
initialize_app()
|
||||||
|
seed_txt = APP_DIR / f"{profile_name}_seed.txt"
|
||||||
|
if seed_txt.exists():
|
||||||
|
seed_phrase = seed_txt.read_text().strip()
|
||||||
|
else:
|
||||||
|
seed_phrase = (
|
||||||
|
Bip39MnemonicGenerator(Bip39Languages.ENGLISH)
|
||||||
|
.FromWordsNumber(Bip39WordsNum.WORDS_NUM_12)
|
||||||
|
.ToStr()
|
||||||
|
)
|
||||||
|
seed_txt.write_text(seed_phrase)
|
||||||
|
seed_txt.chmod(0o600)
|
||||||
|
|
||||||
|
fp_mgr = FingerprintManager(APP_DIR)
|
||||||
|
fingerprint = fp_mgr.add_fingerprint(seed_phrase) or generate_fingerprint(
|
||||||
|
seed_phrase
|
||||||
|
)
|
||||||
|
if fingerprint is None:
|
||||||
|
fingerprint = profile_name
|
||||||
|
profile_dir = APP_DIR / fingerprint
|
||||||
|
profile_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
seed_key = derive_key_from_password(DEFAULT_PASSWORD)
|
||||||
|
seed_mgr = EncryptionManager(seed_key, profile_dir)
|
||||||
|
seed_file = profile_dir / "parent_seed.enc"
|
||||||
|
clear_path = profile_dir / "seed_phrase.txt"
|
||||||
|
|
||||||
|
if seed_file.exists():
|
||||||
|
try:
|
||||||
|
current = seed_mgr.decrypt_parent_seed()
|
||||||
|
except Exception:
|
||||||
|
current = None
|
||||||
|
if current != seed_phrase:
|
||||||
|
seed_mgr.encrypt_parent_seed(seed_phrase)
|
||||||
|
else:
|
||||||
|
seed_mgr.encrypt_parent_seed(seed_phrase)
|
||||||
|
|
||||||
|
if not clear_path.exists():
|
||||||
|
clear_path.write_text(seed_phrase)
|
||||||
|
clear_path.chmod(0o600)
|
||||||
|
|
||||||
|
index_key = derive_index_key(seed_phrase)
|
||||||
|
enc_mgr = EncryptionManager(index_key, profile_dir)
|
||||||
|
vault = Vault(enc_mgr, profile_dir)
|
||||||
|
cfg_mgr = ConfigManager(vault, profile_dir)
|
||||||
|
# Store the default password hash so the profile can be opened
|
||||||
|
hashed = bcrypt.hashpw(DEFAULT_PASSWORD.encode(), bcrypt.gensalt()).decode()
|
||||||
|
cfg_mgr.set_password_hash(hashed)
|
||||||
|
backup_mgr = BackupManager(profile_dir, cfg_mgr)
|
||||||
|
entry_mgr = EntryManager(vault, backup_mgr)
|
||||||
|
return seed_phrase, entry_mgr, profile_dir, fingerprint
|
||||||
|
|
||||||
|
|
||||||
|
def random_secret(length: int = 16) -> str:
|
||||||
|
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
|
||||||
|
return "".join(random.choice(alphabet) for _ in range(length))
|
||||||
|
|
||||||
|
|
||||||
|
def populate(entry_mgr: EntryManager, seed: str, count: int) -> None:
|
||||||
|
"""Add ``count`` entries of varying types to the vault."""
|
||||||
|
start_index = entry_mgr.get_next_index()
|
||||||
|
for i in range(count):
|
||||||
|
idx = start_index + i
|
||||||
|
kind = idx % 7
|
||||||
|
if kind == 0:
|
||||||
|
entry_mgr.add_entry(
|
||||||
|
label=f"site-{idx}.example.com",
|
||||||
|
length=12,
|
||||||
|
username=f"user{idx}",
|
||||||
|
url=f"https://example{idx}.com",
|
||||||
|
notes=f"Website account {idx}",
|
||||||
|
custom_fields=[{"key": "id", "value": str(idx)}],
|
||||||
|
)
|
||||||
|
elif kind == 1:
|
||||||
|
entry_mgr.add_totp(f"totp-generated-{idx}", seed)
|
||||||
|
elif kind == 2:
|
||||||
|
entry_mgr.add_totp(f"totp-imported-{idx}", seed, secret=random_secret())
|
||||||
|
elif kind == 3:
|
||||||
|
entry_mgr.add_ssh_key(f"ssh-{idx}", seed, notes=f"SSH key for server {idx}")
|
||||||
|
elif kind == 4:
|
||||||
|
entry_mgr.add_seed(
|
||||||
|
f"derived-seed-{idx}", seed, words_num=24, notes=f"Seed {idx}"
|
||||||
|
)
|
||||||
|
elif kind == 5:
|
||||||
|
entry_mgr.add_nostr_key(f"nostr-{idx}", notes=f"Nostr key {idx}")
|
||||||
|
else:
|
||||||
|
entry_mgr.add_pgp_key(
|
||||||
|
f"pgp-{idx}",
|
||||||
|
seed,
|
||||||
|
user_id=f"user{idx}@example.com",
|
||||||
|
notes=f"PGP key {idx}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Create or extend a SeedPass test profile"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--profile",
|
||||||
|
default="test_profile",
|
||||||
|
help="profile name inside ~/.seedpass/tests",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--count",
|
||||||
|
type=int,
|
||||||
|
default=100,
|
||||||
|
help="number of entries to add",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
seed, entry_mgr, dir_path, fingerprint = initialize_profile(args.profile)
|
||||||
|
print(f"Using profile directory: {dir_path}")
|
||||||
|
print(f"Parent seed: {seed}")
|
||||||
|
if fingerprint:
|
||||||
|
print(f"Fingerprint: {fingerprint}")
|
||||||
|
populate(entry_mgr, seed, args.count)
|
||||||
|
print(f"Added {args.count} entries.")
|
||||||
|
|
||||||
|
encrypted = entry_mgr.vault.get_encrypted_index()
|
||||||
|
if encrypted:
|
||||||
|
client = NostrClient(
|
||||||
|
entry_mgr.vault.encryption_manager,
|
||||||
|
fingerprint or dir_path.name,
|
||||||
|
parent_seed=seed,
|
||||||
|
)
|
||||||
|
asyncio.run(client.publish_snapshot(encrypted))
|
||||||
|
print("[+] Data synchronized to Nostr.")
|
||||||
|
try:
|
||||||
|
result = asyncio.run(client.fetch_latest_snapshot())
|
||||||
|
if result:
|
||||||
|
_, chunks = result
|
||||||
|
retrieved = gzip.decompress(b"".join(chunks))
|
||||||
|
if retrieved == encrypted:
|
||||||
|
print("[+] Verified snapshot retrieval.")
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"[!] Retrieval failed: {client.last_error or 'data mismatch'}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(f"[!] Retrieval failed: {client.last_error or 'data mismatch'}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[!] Retrieval failed: {e}")
|
||||||
|
else:
|
||||||
|
print("[-] No encrypted index found to sync.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
10
src/main.py
10
src/main.py
@@ -304,6 +304,8 @@ def handle_post_to_nostr(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Failed to post to Nostr: {e}", exc_info=True)
|
logging.error(f"Failed to post to Nostr: {e}", exc_info=True)
|
||||||
print(colored(f"Error: Failed to post to Nostr: {e}", "red"))
|
print(colored(f"Error: Failed to post to Nostr: {e}", "red"))
|
||||||
|
finally:
|
||||||
|
pause()
|
||||||
|
|
||||||
|
|
||||||
def handle_retrieve_from_nostr(password_manager: PasswordManager):
|
def handle_retrieve_from_nostr(password_manager: PasswordManager):
|
||||||
@@ -336,6 +338,8 @@ def handle_retrieve_from_nostr(password_manager: PasswordManager):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Failed to retrieve from Nostr: {e}", exc_info=True)
|
logging.error(f"Failed to retrieve from Nostr: {e}", exc_info=True)
|
||||||
print(colored(f"Error: Failed to retrieve from Nostr: {e}", "red"))
|
print(colored(f"Error: Failed to retrieve from Nostr: {e}", "red"))
|
||||||
|
finally:
|
||||||
|
pause()
|
||||||
|
|
||||||
|
|
||||||
def handle_view_relays(cfg_mgr: "ConfigManager") -> None:
|
def handle_view_relays(cfg_mgr: "ConfigManager") -> None:
|
||||||
@@ -395,6 +399,8 @@ def handle_add_relay(password_manager: PasswordManager) -> None:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error adding relay: {e}")
|
logging.error(f"Error adding relay: {e}")
|
||||||
print(colored(f"Error: {e}", "red"))
|
print(colored(f"Error: {e}", "red"))
|
||||||
|
finally:
|
||||||
|
pause()
|
||||||
|
|
||||||
|
|
||||||
def handle_remove_relay(password_manager: PasswordManager) -> None:
|
def handle_remove_relay(password_manager: PasswordManager) -> None:
|
||||||
@@ -430,6 +436,8 @@ def handle_remove_relay(password_manager: PasswordManager) -> None:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error removing relay: {e}")
|
logging.error(f"Error removing relay: {e}")
|
||||||
print(colored(f"Error: {e}", "red"))
|
print(colored(f"Error: {e}", "red"))
|
||||||
|
finally:
|
||||||
|
pause()
|
||||||
|
|
||||||
|
|
||||||
def handle_reset_relays(password_manager: PasswordManager) -> None:
|
def handle_reset_relays(password_manager: PasswordManager) -> None:
|
||||||
@@ -447,6 +455,8 @@ def handle_reset_relays(password_manager: PasswordManager) -> None:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error resetting relays: {e}")
|
logging.error(f"Error resetting relays: {e}")
|
||||||
print(colored(f"Error: {e}", "red"))
|
print(colored(f"Error: {e}", "red"))
|
||||||
|
finally:
|
||||||
|
pause()
|
||||||
|
|
||||||
|
|
||||||
def handle_set_inactivity_timeout(password_manager: PasswordManager) -> None:
|
def handle_set_inactivity_timeout(password_manager: PasswordManager) -> None:
|
||||||
|
@@ -153,10 +153,8 @@ class NostrClient:
|
|||||||
while True:
|
while True:
|
||||||
msg = await asyncio.wait_for(ws.recv(), timeout=timeout)
|
msg = await asyncio.wait_for(ws.recv(), timeout=timeout)
|
||||||
data = json.loads(msg)
|
data = json.loads(msg)
|
||||||
if data[0] == "EVENT":
|
if data[0] in {"EVENT", "EOSE"}:
|
||||||
return True
|
return True
|
||||||
if data[0] == "EOSE":
|
|
||||||
return False
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@@ -376,14 +376,10 @@ class EncryptionManager:
|
|||||||
try:
|
try:
|
||||||
relative_path = Path("seedpass_entries_db.json.enc")
|
relative_path = Path("seedpass_entries_db.json.enc")
|
||||||
if not (self.fingerprint_dir / relative_path).exists():
|
if not (self.fingerprint_dir / relative_path).exists():
|
||||||
logger.error(
|
# Missing index is normal on first run
|
||||||
|
logger.info(
|
||||||
f"Index file '{relative_path}' does not exist in '{self.fingerprint_dir}'."
|
f"Index file '{relative_path}' does not exist in '{self.fingerprint_dir}'."
|
||||||
)
|
)
|
||||||
print(
|
|
||||||
colored(
|
|
||||||
f"Error: Index file '{relative_path}' does not exist.", "red"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
file_path = self.fingerprint_dir / relative_path
|
file_path = self.fingerprint_dir / relative_path
|
||||||
|
@@ -302,7 +302,7 @@ class PasswordManager:
|
|||||||
# Initialize BIP85 and other managers
|
# Initialize BIP85 and other managers
|
||||||
self.initialize_bip85()
|
self.initialize_bip85()
|
||||||
self.initialize_managers()
|
self.initialize_managers()
|
||||||
self.sync_index_from_nostr_if_missing()
|
self.sync_index_from_nostr()
|
||||||
print(
|
print(
|
||||||
colored(
|
colored(
|
||||||
f"Seed profile {fingerprint} selected and managers initialized.",
|
f"Seed profile {fingerprint} selected and managers initialized.",
|
||||||
@@ -432,7 +432,7 @@ class PasswordManager:
|
|||||||
# Initialize BIP85 and other managers
|
# Initialize BIP85 and other managers
|
||||||
self.initialize_bip85()
|
self.initialize_bip85()
|
||||||
self.initialize_managers()
|
self.initialize_managers()
|
||||||
self.sync_index_from_nostr_if_missing()
|
self.sync_index_from_nostr()
|
||||||
print(colored(f"Switched to seed profile {selected_fingerprint}.", "green"))
|
print(colored(f"Switched to seed profile {selected_fingerprint}.", "green"))
|
||||||
|
|
||||||
# Re-initialize NostrClient with the new fingerprint
|
# Re-initialize NostrClient with the new fingerprint
|
||||||
@@ -616,7 +616,7 @@ class PasswordManager:
|
|||||||
|
|
||||||
self.initialize_bip85()
|
self.initialize_bip85()
|
||||||
self.initialize_managers()
|
self.initialize_managers()
|
||||||
self.sync_index_from_nostr_if_missing()
|
self.sync_index_from_nostr()
|
||||||
return fingerprint # Return the generated or added fingerprint
|
return fingerprint # Return the generated or added fingerprint
|
||||||
else:
|
else:
|
||||||
logging.error("Invalid BIP-85 seed phrase. Exiting.")
|
logging.error("Invalid BIP-85 seed phrase. Exiting.")
|
||||||
@@ -757,7 +757,7 @@ class PasswordManager:
|
|||||||
|
|
||||||
self.initialize_bip85()
|
self.initialize_bip85()
|
||||||
self.initialize_managers()
|
self.initialize_managers()
|
||||||
self.sync_index_from_nostr_if_missing()
|
self.sync_index_from_nostr()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Failed to encrypt and save parent seed: {e}", exc_info=True)
|
logging.error(f"Failed to encrypt and save parent seed: {e}", exc_info=True)
|
||||||
print(colored(f"Error: Failed to encrypt and save parent seed: {e}", "red"))
|
print(colored(f"Error: Failed to encrypt and save parent seed: {e}", "red"))
|
||||||
|
32
src/tests/test_generate_test_profile.py
Normal file
32
src/tests/test_generate_test_profile.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import importlib
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
|
||||||
|
def test_initialize_profile_creates_directories(monkeypatch):
|
||||||
|
with TemporaryDirectory() as tmpdir:
|
||||||
|
tmp_path = Path(tmpdir)
|
||||||
|
# Mock home directory so APP_DIR is within tmp_path
|
||||||
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||||
|
|
||||||
|
# Reload constants to use the mocked home directory
|
||||||
|
constants = importlib.import_module("constants")
|
||||||
|
importlib.reload(constants)
|
||||||
|
# Load the script module directly from its path
|
||||||
|
script_path = (
|
||||||
|
Path(__file__).resolve().parents[2] / "scripts" / "generate_test_profile.py"
|
||||||
|
)
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
"generate_test_profile", script_path
|
||||||
|
)
|
||||||
|
gtp = importlib.util.module_from_spec(spec)
|
||||||
|
assert spec.loader is not None
|
||||||
|
spec.loader.exec_module(gtp)
|
||||||
|
|
||||||
|
seed, mgr, dir_path, fingerprint = gtp.initialize_profile("test")
|
||||||
|
|
||||||
|
assert constants.APP_DIR.exists()
|
||||||
|
assert (constants.APP_DIR / "test_seed.txt").exists()
|
||||||
|
assert dir_path.exists()
|
||||||
|
assert dir_path.name == fingerprint
|
@@ -2,12 +2,15 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
from cryptography.fernet import Fernet
|
from cryptography.fernet import Fernet
|
||||||
|
|
||||||
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
||||||
|
|
||||||
from password_manager.encryption import EncryptionManager
|
from password_manager.encryption import EncryptionManager
|
||||||
from nostr.client import NostrClient
|
from nostr.client import NostrClient
|
||||||
|
import nostr.client as nostr_client
|
||||||
|
|
||||||
|
|
||||||
def test_nostr_client_uses_custom_relays():
|
def test_nostr_client_uses_custom_relays():
|
||||||
@@ -50,6 +53,25 @@ class FakeAddRelayClient:
|
|||||||
self.connected = True
|
self.connected = True
|
||||||
|
|
||||||
|
|
||||||
|
class FakeWebSocket:
|
||||||
|
def __init__(self, messages):
|
||||||
|
self.messages = messages
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc, tb):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def send(self, _):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def recv(self):
|
||||||
|
if self.messages:
|
||||||
|
return self.messages.pop(0)
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
|
||||||
|
|
||||||
def _setup_client(tmpdir, fake_cls):
|
def _setup_client(tmpdir, fake_cls):
|
||||||
key = Fernet.generate_key()
|
key = Fernet.generate_key()
|
||||||
enc_mgr = EncryptionManager(key, Path(tmpdir))
|
enc_mgr = EncryptionManager(key, Path(tmpdir))
|
||||||
@@ -91,3 +113,18 @@ def test_check_relay_health_runs_async(tmp_path, monkeypatch):
|
|||||||
|
|
||||||
assert result == 1
|
assert result == 1
|
||||||
assert recorded["args"] == (3, 2)
|
assert recorded["args"] == (3, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ping_relay_accepts_eose(tmp_path, monkeypatch):
|
||||||
|
client = _setup_client(tmp_path, FakeAddRelayClient)
|
||||||
|
|
||||||
|
fake_ws = FakeWebSocket([json.dumps(["EOSE"])])
|
||||||
|
|
||||||
|
def fake_connect(*_args, **_kwargs):
|
||||||
|
return fake_ws
|
||||||
|
|
||||||
|
monkeypatch.setattr(nostr_client.websockets, "connect", fake_connect)
|
||||||
|
|
||||||
|
result = asyncio.run(client._ping_relay("wss://relay", timeout=0.1))
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
Reference in New Issue
Block a user