mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-09 15:58:48 +00:00
Merge pull request #507 from PR0M3TH3AN/codex/reduce-max-retries-and-retry-delay
Add configurable Nostr retry settings
This commit is contained in:
@@ -417,6 +417,8 @@ You can adjust these settings directly from the command line:
|
|||||||
seedpass config set kdf_iterations 200000
|
seedpass config set kdf_iterations 200000
|
||||||
seedpass config set backup_interval 3600
|
seedpass config set backup_interval 3600
|
||||||
seedpass config set quick_unlock true
|
seedpass config set quick_unlock true
|
||||||
|
seedpass config set nostr_max_retries 2
|
||||||
|
seedpass config set nostr_retry_delay 1
|
||||||
```
|
```
|
||||||
|
|
||||||
The default configuration uses **50,000** PBKDF2 iterations. Lower iteration counts speed up vault decryption but make brute-force attacks easier. A long backup interval means fewer backups and increases the risk of data loss.
|
The default configuration uses **50,000** PBKDF2 iterations. Lower iteration counts speed up vault decryption but make brute-force attacks easier. A long backup interval means fewer backups and increases the risk of data loss.
|
||||||
|
@@ -172,8 +172,8 @@ Code: 123456
|
|||||||
|
|
||||||
### `config` Commands
|
### `config` Commands
|
||||||
|
|
||||||
- **`seedpass config get <key>`** – Retrieve a configuration value such as `kdf_iterations`, `backup_interval`, `inactivity_timeout`, `secret_mode_enabled`, `clipboard_clear_delay`, `additional_backup_path`, `relays`, `quick_unlock`, or password policy fields like `min_uppercase`.
|
- **`seedpass config get <key>`** – Retrieve a configuration value such as `kdf_iterations`, `backup_interval`, `inactivity_timeout`, `secret_mode_enabled`, `clipboard_clear_delay`, `additional_backup_path`, `relays`, `quick_unlock`, `nostr_max_retries`, `nostr_retry_delay`, or password policy fields like `min_uppercase`.
|
||||||
- **`seedpass config set <key> <value>`** – Update a configuration option. Example: `seedpass config set kdf_iterations 200000`. Use keys like `min_uppercase`, `min_lowercase`, `min_digits`, `min_special`, or `quick_unlock` to adjust settings.
|
- **`seedpass config set <key> <value>`** – Update a configuration option. Example: `seedpass config set kdf_iterations 200000`. Use keys like `min_uppercase`, `min_lowercase`, `min_digits`, `min_special`, `nostr_max_retries`, `nostr_retry_delay`, or `quick_unlock` to adjust settings.
|
||||||
- **`seedpass config toggle-secret-mode`** – Interactively enable or disable Secret Mode and set the clipboard delay.
|
- **`seedpass config toggle-secret-mode`** – Interactively enable or disable Secret Mode and set the clipboard delay.
|
||||||
- **`seedpass config toggle-offline`** – Enable or disable offline mode to skip Nostr operations.
|
- **`seedpass config toggle-offline`** – Enable or disable offline mode to skip Nostr operations.
|
||||||
|
|
||||||
@@ -210,6 +210,6 @@ Shut down the server with `seedpass api stop`.
|
|||||||
|
|
||||||
- Use the `--help` flag for details on any command.
|
- Use the `--help` flag for details on any command.
|
||||||
- Set a strong master password and regularly export encrypted backups.
|
- Set a strong master password and regularly export encrypted backups.
|
||||||
- Adjust configuration values like `kdf_iterations`, `backup_interval`, `inactivity_timeout`, `secret_mode_enabled`, or `quick_unlock` through the `config` commands.
|
- Adjust configuration values like `kdf_iterations`, `backup_interval`, `inactivity_timeout`, `secret_mode_enabled`, `nostr_max_retries`, `nostr_retry_delay`, or `quick_unlock` through the `config` commands.
|
||||||
- Customize password complexity with `config set min_uppercase 3`, `config set min_digits 4`, and similar commands.
|
- Customize password complexity with `config set min_uppercase 3`, `config set min_digits 4`, and similar commands.
|
||||||
- `entry get` is script‑friendly and can be piped into other commands.
|
- `entry get` is script‑friendly and can be piped into other commands.
|
||||||
|
@@ -46,7 +46,9 @@ import gzip
|
|||||||
DEFAULT_PASSWORD = "testpassword"
|
DEFAULT_PASSWORD = "testpassword"
|
||||||
|
|
||||||
|
|
||||||
def initialize_profile(profile_name: str) -> tuple[str, EntryManager, Path, str]:
|
def initialize_profile(
|
||||||
|
profile_name: str,
|
||||||
|
) -> tuple[str, EntryManager, Path, str, ConfigManager]:
|
||||||
"""Create or load a profile and return the seed phrase, manager, directory and fingerprint."""
|
"""Create or load a profile and return the seed phrase, manager, directory and fingerprint."""
|
||||||
initialize_app()
|
initialize_app()
|
||||||
seed_txt = APP_DIR / f"{profile_name}_seed.txt"
|
seed_txt = APP_DIR / f"{profile_name}_seed.txt"
|
||||||
@@ -98,7 +100,7 @@ def initialize_profile(profile_name: str) -> tuple[str, EntryManager, Path, str]
|
|||||||
cfg_mgr.set_password_hash(hashed)
|
cfg_mgr.set_password_hash(hashed)
|
||||||
backup_mgr = BackupManager(profile_dir, cfg_mgr)
|
backup_mgr = BackupManager(profile_dir, cfg_mgr)
|
||||||
entry_mgr = EntryManager(vault, backup_mgr)
|
entry_mgr = EntryManager(vault, backup_mgr)
|
||||||
return seed_phrase, entry_mgr, profile_dir, fingerprint
|
return seed_phrase, entry_mgr, profile_dir, fingerprint, cfg_mgr
|
||||||
|
|
||||||
|
|
||||||
def random_secret(length: int = 16) -> str:
|
def random_secret(length: int = 16) -> str:
|
||||||
@@ -159,7 +161,7 @@ def main() -> None:
|
|||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
seed, entry_mgr, dir_path, fingerprint = initialize_profile(args.profile)
|
seed, entry_mgr, dir_path, fingerprint, cfg_mgr = initialize_profile(args.profile)
|
||||||
print(f"Using profile directory: {dir_path}")
|
print(f"Using profile directory: {dir_path}")
|
||||||
print(f"Parent seed: {seed}")
|
print(f"Parent seed: {seed}")
|
||||||
if fingerprint:
|
if fingerprint:
|
||||||
@@ -173,6 +175,7 @@ def main() -> None:
|
|||||||
entry_mgr.vault.encryption_manager,
|
entry_mgr.vault.encryption_manager,
|
||||||
fingerprint or dir_path.name,
|
fingerprint or dir_path.name,
|
||||||
parent_seed=seed,
|
parent_seed=seed,
|
||||||
|
config_manager=cfg_mgr,
|
||||||
)
|
)
|
||||||
asyncio.run(client.publish_snapshot(encrypted))
|
asyncio.run(client.publish_snapshot(encrypted))
|
||||||
print("[+] Data synchronized to Nostr.")
|
print("[+] Data synchronized to Nostr.")
|
||||||
|
@@ -9,8 +9,9 @@ logger = logging.getLogger(__name__)
|
|||||||
# -----------------------------------
|
# -----------------------------------
|
||||||
# Nostr Relay Connection Settings
|
# Nostr Relay Connection Settings
|
||||||
# -----------------------------------
|
# -----------------------------------
|
||||||
MAX_RETRIES = 3 # Maximum number of retries for relay connections
|
# Retry fewer times with a shorter wait by default
|
||||||
RETRY_DELAY = 5 # Seconds to wait before retrying a failed connection
|
MAX_RETRIES = 2 # Maximum number of retries for relay connections
|
||||||
|
RETRY_DELAY = 1 # Seconds to wait before retrying a failed connection
|
||||||
MIN_HEALTHY_RELAYS = 2 # Minimum relays that should return data on startup
|
MIN_HEALTHY_RELAYS = 2 # Minimum relays that should return data on startup
|
||||||
|
|
||||||
# -----------------------------------
|
# -----------------------------------
|
||||||
|
@@ -4,7 +4,7 @@ import base64
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from typing import List, Optional, Tuple
|
from typing import List, Optional, Tuple, TYPE_CHECKING
|
||||||
import hashlib
|
import hashlib
|
||||||
import asyncio
|
import asyncio
|
||||||
import gzip
|
import gzip
|
||||||
@@ -27,8 +27,12 @@ from nostr_sdk import EventId, Timestamp
|
|||||||
from .key_manager import KeyManager as SeedPassKeyManager
|
from .key_manager import KeyManager as SeedPassKeyManager
|
||||||
from .backup_models import Manifest, ChunkMeta, KIND_MANIFEST, KIND_SNAPSHOT_CHUNK
|
from .backup_models import Manifest, ChunkMeta, KIND_MANIFEST, KIND_SNAPSHOT_CHUNK
|
||||||
from password_manager.encryption import EncryptionManager
|
from password_manager.encryption import EncryptionManager
|
||||||
|
from constants import MAX_RETRIES, RETRY_DELAY
|
||||||
from utils.file_lock import exclusive_lock
|
from utils.file_lock import exclusive_lock
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # pragma: no cover - imported for type hints
|
||||||
|
from password_manager.config_manager import ConfigManager
|
||||||
|
|
||||||
# Backwards compatibility for tests that patch these symbols
|
# Backwards compatibility for tests that patch these symbols
|
||||||
KeyManager = SeedPassKeyManager
|
KeyManager = SeedPassKeyManager
|
||||||
ClientBuilder = Client
|
ClientBuilder = Client
|
||||||
@@ -91,10 +95,12 @@ class NostrClient:
|
|||||||
relays: Optional[List[str]] = None,
|
relays: Optional[List[str]] = None,
|
||||||
parent_seed: Optional[str] = None,
|
parent_seed: Optional[str] = None,
|
||||||
offline_mode: bool = False,
|
offline_mode: bool = False,
|
||||||
|
config_manager: Optional["ConfigManager"] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.encryption_manager = encryption_manager
|
self.encryption_manager = encryption_manager
|
||||||
self.fingerprint = fingerprint
|
self.fingerprint = fingerprint
|
||||||
self.fingerprint_dir = self.encryption_manager.fingerprint_dir
|
self.fingerprint_dir = self.encryption_manager.fingerprint_dir
|
||||||
|
self.config_manager = config_manager
|
||||||
|
|
||||||
if parent_seed is None:
|
if parent_seed is None:
|
||||||
parent_seed = self.encryption_manager.decrypt_parent_seed()
|
parent_seed = self.encryption_manager.decrypt_parent_seed()
|
||||||
@@ -270,11 +276,27 @@ class NostrClient:
|
|||||||
self._connected = False
|
self._connected = False
|
||||||
|
|
||||||
def retrieve_json_from_nostr_sync(
|
def retrieve_json_from_nostr_sync(
|
||||||
self, retries: int = 0, delay: float = 2.0
|
self, retries: int | None = None, delay: float | None = None
|
||||||
) -> Optional[bytes]:
|
) -> Optional[bytes]:
|
||||||
"""Retrieve the latest Kind 1 event from the author with optional retries."""
|
"""Retrieve the latest Kind 1 event from the author with optional retries."""
|
||||||
if self.offline_mode or not self.relays:
|
if self.offline_mode or not self.relays:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if retries is None or delay is None:
|
||||||
|
if self.config_manager is None:
|
||||||
|
from password_manager.config_manager import ConfigManager
|
||||||
|
from password_manager.vault import Vault
|
||||||
|
|
||||||
|
cfg_mgr = ConfigManager(
|
||||||
|
Vault(self.encryption_manager, self.fingerprint_dir),
|
||||||
|
self.fingerprint_dir,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cfg_mgr = self.config_manager
|
||||||
|
cfg = cfg_mgr.load_config(require_pin=False)
|
||||||
|
retries = int(cfg.get("nostr_max_retries", MAX_RETRIES))
|
||||||
|
delay = float(cfg.get("nostr_retry_delay", RETRY_DELAY))
|
||||||
|
|
||||||
self.connect()
|
self.connect()
|
||||||
self.last_error = None
|
self.last_error = None
|
||||||
attempt = 0
|
attempt = 0
|
||||||
|
@@ -52,6 +52,8 @@ class ConfigManager:
|
|||||||
"secret_mode_enabled": False,
|
"secret_mode_enabled": False,
|
||||||
"clipboard_clear_delay": 45,
|
"clipboard_clear_delay": 45,
|
||||||
"quick_unlock": False,
|
"quick_unlock": False,
|
||||||
|
"nostr_max_retries": 2,
|
||||||
|
"nostr_retry_delay": 1.0,
|
||||||
"min_uppercase": 2,
|
"min_uppercase": 2,
|
||||||
"min_lowercase": 2,
|
"min_lowercase": 2,
|
||||||
"min_digits": 2,
|
"min_digits": 2,
|
||||||
@@ -74,6 +76,8 @@ class ConfigManager:
|
|||||||
data.setdefault("secret_mode_enabled", False)
|
data.setdefault("secret_mode_enabled", False)
|
||||||
data.setdefault("clipboard_clear_delay", 45)
|
data.setdefault("clipboard_clear_delay", 45)
|
||||||
data.setdefault("quick_unlock", False)
|
data.setdefault("quick_unlock", False)
|
||||||
|
data.setdefault("nostr_max_retries", 2)
|
||||||
|
data.setdefault("nostr_retry_delay", 1.0)
|
||||||
data.setdefault("min_uppercase", 2)
|
data.setdefault("min_uppercase", 2)
|
||||||
data.setdefault("min_lowercase", 2)
|
data.setdefault("min_lowercase", 2)
|
||||||
data.setdefault("min_digits", 2)
|
data.setdefault("min_digits", 2)
|
||||||
@@ -285,3 +289,29 @@ class ConfigManager:
|
|||||||
"""Retrieve whether quick unlock is enabled."""
|
"""Retrieve whether quick unlock is enabled."""
|
||||||
cfg = self.load_config(require_pin=False)
|
cfg = self.load_config(require_pin=False)
|
||||||
return bool(cfg.get("quick_unlock", False))
|
return bool(cfg.get("quick_unlock", False))
|
||||||
|
|
||||||
|
def set_nostr_max_retries(self, retries: int) -> None:
|
||||||
|
"""Persist the maximum number of Nostr retry attempts."""
|
||||||
|
if retries < 0:
|
||||||
|
raise ValueError("retries cannot be negative")
|
||||||
|
cfg = self.load_config(require_pin=False)
|
||||||
|
cfg["nostr_max_retries"] = int(retries)
|
||||||
|
self.save_config(cfg)
|
||||||
|
|
||||||
|
def get_nostr_max_retries(self) -> int:
|
||||||
|
"""Retrieve the configured Nostr retry count."""
|
||||||
|
cfg = self.load_config(require_pin=False)
|
||||||
|
return int(cfg.get("nostr_max_retries", 2))
|
||||||
|
|
||||||
|
def set_nostr_retry_delay(self, delay: float) -> None:
|
||||||
|
"""Persist the delay between Nostr retry attempts."""
|
||||||
|
if delay < 0:
|
||||||
|
raise ValueError("delay cannot be negative")
|
||||||
|
cfg = self.load_config(require_pin=False)
|
||||||
|
cfg["nostr_retry_delay"] = float(delay)
|
||||||
|
self.save_config(cfg)
|
||||||
|
|
||||||
|
def get_nostr_retry_delay(self) -> float:
|
||||||
|
"""Retrieve the delay in seconds between Nostr retries."""
|
||||||
|
cfg = self.load_config(require_pin=False)
|
||||||
|
return float(cfg.get("nostr_retry_delay", 1.0))
|
||||||
|
@@ -544,6 +544,7 @@ class PasswordManager:
|
|||||||
self.nostr_client = NostrClient(
|
self.nostr_client = NostrClient(
|
||||||
encryption_manager=self.encryption_manager,
|
encryption_manager=self.encryption_manager,
|
||||||
fingerprint=self.current_fingerprint,
|
fingerprint=self.current_fingerprint,
|
||||||
|
config_manager=getattr(self, "config_manager", None),
|
||||||
parent_seed=getattr(self, "parent_seed", None),
|
parent_seed=getattr(self, "parent_seed", None),
|
||||||
)
|
)
|
||||||
logging.info(
|
logging.info(
|
||||||
@@ -1020,6 +1021,7 @@ class PasswordManager:
|
|||||||
fingerprint=self.current_fingerprint,
|
fingerprint=self.current_fingerprint,
|
||||||
relays=relay_list,
|
relays=relay_list,
|
||||||
offline_mode=self.offline_mode,
|
offline_mode=self.offline_mode,
|
||||||
|
config_manager=self.config_manager,
|
||||||
parent_seed=getattr(self, "parent_seed", None),
|
parent_seed=getattr(self, "parent_seed", None),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3718,6 +3720,7 @@ class PasswordManager:
|
|||||||
encryption_manager=self.encryption_manager,
|
encryption_manager=self.encryption_manager,
|
||||||
fingerprint=self.current_fingerprint,
|
fingerprint=self.current_fingerprint,
|
||||||
relays=relay_list,
|
relays=relay_list,
|
||||||
|
config_manager=self.config_manager,
|
||||||
parent_seed=getattr(self, "parent_seed", None),
|
parent_seed=getattr(self, "parent_seed", None),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -90,7 +90,11 @@ def export_backup(
|
|||||||
enc_file.write_bytes(encrypted)
|
enc_file.write_bytes(encrypted)
|
||||||
os.chmod(enc_file, 0o600)
|
os.chmod(enc_file, 0o600)
|
||||||
try:
|
try:
|
||||||
client = NostrClient(vault.encryption_manager, vault.fingerprint_dir.name)
|
client = NostrClient(
|
||||||
|
vault.encryption_manager,
|
||||||
|
vault.fingerprint_dir.name,
|
||||||
|
config_manager=backup_manager.config_manager,
|
||||||
|
)
|
||||||
asyncio.run(client.publish_snapshot(encrypted))
|
asyncio.run(client.publish_snapshot(encrypted))
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error("Failed to publish backup via Nostr", exc_info=True)
|
logger.error("Failed to publish backup via Nostr", exc_info=True)
|
||||||
|
@@ -464,6 +464,8 @@ def config_set(ctx: typer.Context, key: str, value: str) -> None:
|
|||||||
"kdf_iterations": lambda v: cfg.set_kdf_iterations(int(v)),
|
"kdf_iterations": lambda v: cfg.set_kdf_iterations(int(v)),
|
||||||
"kdf_mode": lambda v: cfg.set_kdf_mode(v),
|
"kdf_mode": lambda v: cfg.set_kdf_mode(v),
|
||||||
"backup_interval": lambda v: cfg.set_backup_interval(float(v)),
|
"backup_interval": lambda v: cfg.set_backup_interval(float(v)),
|
||||||
|
"nostr_max_retries": lambda v: cfg.set_nostr_max_retries(int(v)),
|
||||||
|
"nostr_retry_delay": lambda v: cfg.set_nostr_retry_delay(float(v)),
|
||||||
"min_uppercase": lambda v: cfg.set_min_uppercase(int(v)),
|
"min_uppercase": lambda v: cfg.set_min_uppercase(int(v)),
|
||||||
"min_lowercase": lambda v: cfg.set_min_lowercase(int(v)),
|
"min_lowercase": lambda v: cfg.set_min_lowercase(int(v)),
|
||||||
"min_digits": lambda v: cfg.set_min_digits(int(v)),
|
"min_digits": lambda v: cfg.set_min_digits(int(v)),
|
||||||
|
@@ -18,6 +18,8 @@ runner = CliRunner()
|
|||||||
("kdf_iterations", "123", "set_kdf_iterations", 123),
|
("kdf_iterations", "123", "set_kdf_iterations", 123),
|
||||||
("kdf_mode", "argon2", "set_kdf_mode", "argon2"),
|
("kdf_mode", "argon2", "set_kdf_mode", "argon2"),
|
||||||
("quick_unlock", "true", "set_quick_unlock", True),
|
("quick_unlock", "true", "set_quick_unlock", True),
|
||||||
|
("nostr_max_retries", "3", "set_nostr_max_retries", 3),
|
||||||
|
("nostr_retry_delay", "1.5", "set_nostr_retry_delay", 1.5),
|
||||||
(
|
(
|
||||||
"relays",
|
"relays",
|
||||||
"wss://a.com, wss://b.com",
|
"wss://a.com, wss://b.com",
|
||||||
|
@@ -63,6 +63,8 @@ class DummyPM:
|
|||||||
set_clipboard_clear_delay=lambda v: None,
|
set_clipboard_clear_delay=lambda v: None,
|
||||||
set_additional_backup_path=lambda v: None,
|
set_additional_backup_path=lambda v: None,
|
||||||
set_relays=lambda v, require_pin=False: None,
|
set_relays=lambda v, require_pin=False: None,
|
||||||
|
set_nostr_max_retries=lambda v: None,
|
||||||
|
set_nostr_retry_delay=lambda v: None,
|
||||||
set_offline_mode=lambda v: None,
|
set_offline_mode=lambda v: None,
|
||||||
get_secret_mode_enabled=lambda: True,
|
get_secret_mode_enabled=lambda: True,
|
||||||
get_clipboard_clear_delay=lambda: 30,
|
get_clipboard_clear_delay=lambda: 30,
|
||||||
|
@@ -181,3 +181,18 @@ def test_quick_unlock_round_trip():
|
|||||||
|
|
||||||
cfg_mgr.set_quick_unlock(True)
|
cfg_mgr.set_quick_unlock(True)
|
||||||
assert cfg_mgr.get_quick_unlock() is True
|
assert cfg_mgr.get_quick_unlock() is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_nostr_retry_settings_round_trip():
|
||||||
|
with TemporaryDirectory() as tmpdir:
|
||||||
|
vault, _ = create_vault(Path(tmpdir), TEST_SEED, TEST_PASSWORD)
|
||||||
|
cfg_mgr = ConfigManager(vault, Path(tmpdir))
|
||||||
|
|
||||||
|
cfg = cfg_mgr.load_config(require_pin=False)
|
||||||
|
assert cfg["nostr_max_retries"] == 2
|
||||||
|
assert cfg["nostr_retry_delay"] == 1.0
|
||||||
|
|
||||||
|
cfg_mgr.set_nostr_max_retries(5)
|
||||||
|
cfg_mgr.set_nostr_retry_delay(3.5)
|
||||||
|
assert cfg_mgr.get_nostr_max_retries() == 5
|
||||||
|
assert cfg_mgr.get_nostr_retry_delay() == 3.5
|
||||||
|
@@ -24,7 +24,8 @@ def test_initialize_profile_creates_directories(monkeypatch):
|
|||||||
assert spec.loader is not None
|
assert spec.loader is not None
|
||||||
spec.loader.exec_module(gtp)
|
spec.loader.exec_module(gtp)
|
||||||
|
|
||||||
seed, mgr, dir_path, fingerprint = gtp.initialize_profile("test")
|
seed, mgr, dir_path, fingerprint, cfg_mgr = gtp.initialize_profile("test")
|
||||||
|
assert cfg_mgr is not None
|
||||||
|
|
||||||
assert constants.APP_DIR.exists()
|
assert constants.APP_DIR.exists()
|
||||||
assert (constants.APP_DIR / "test_seed.txt").exists()
|
assert (constants.APP_DIR / "test_seed.txt").exists()
|
||||||
|
Reference in New Issue
Block a user