Add configurable password policy

This commit is contained in:
thePR0M3TH3AN
2025-07-13 13:00:02 -04:00
parent c0269801f8
commit b4dfd4b292
9 changed files with 145 additions and 11 deletions

View File

@@ -171,8 +171,8 @@ Code: 123456
### `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`, or `relays`.
- **`seedpass config set <key> <value>`** Update a configuration option. Example: `seedpass config set kdf_iterations 200000`.
- **`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`, 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`, or `min_special` to adjust password complexity.
- **`seedpass config toggle-secret-mode`** Interactively enable or disable Secret Mode and set the clipboard delay.
### `fingerprint` Commands
@@ -209,4 +209,5 @@ Shut down the server with `seedpass api stop`.
- Use the `--help` flag for details on any command.
- Set a strong master password and regularly export encrypted backups.
- Adjust configuration values like `kdf_iterations`, `backup_interval`, `inactivity_timeout`, or `secret_mode_enabled` through the `config` commands.
- Customize password complexity with `config set min_uppercase 3`, `config set min_digits 4`, and similar commands.
- `entry get` is scriptfriendly and can be piped into other commands.

View File

@@ -50,6 +50,10 @@ class ConfigManager:
"backup_interval": 0,
"secret_mode_enabled": False,
"clipboard_clear_delay": 45,
"min_uppercase": 2,
"min_lowercase": 2,
"min_digits": 2,
"min_special": 2,
}
try:
data = self.vault.load_config()
@@ -66,6 +70,10 @@ class ConfigManager:
data.setdefault("backup_interval", 0)
data.setdefault("secret_mode_enabled", False)
data.setdefault("clipboard_clear_delay", 45)
data.setdefault("min_uppercase", 2)
data.setdefault("min_lowercase", 2)
data.setdefault("min_digits", 2)
data.setdefault("min_special", 2)
# Migrate legacy hashed_password.enc if present and password_hash is missing
legacy_file = self.fingerprint_dir / "hashed_password.enc"
@@ -218,3 +226,36 @@ class ConfigManager:
"""Retrieve the backup interval in seconds."""
config = self.load_config(require_pin=False)
return float(config.get("backup_interval", 0))
# Password policy settings
def get_password_policy(self) -> "PasswordPolicy":
"""Return the password complexity policy."""
from password_manager.password_generation import PasswordPolicy
cfg = self.load_config(require_pin=False)
return PasswordPolicy(
min_uppercase=int(cfg.get("min_uppercase", 2)),
min_lowercase=int(cfg.get("min_lowercase", 2)),
min_digits=int(cfg.get("min_digits", 2)),
min_special=int(cfg.get("min_special", 2)),
)
def set_min_uppercase(self, count: int) -> None:
cfg = self.load_config(require_pin=False)
cfg["min_uppercase"] = int(count)
self.save_config(cfg)
def set_min_lowercase(self, count: int) -> None:
cfg = self.load_config(require_pin=False)
cfg["min_lowercase"] = int(count)
self.save_config(cfg)
def set_min_digits(self, count: int) -> None:
cfg = self.load_config(require_pin=False)
cfg["min_digits"] = int(count)
self.save_config(cfg)
def set_min_special(self, count: int) -> None:
cfg = self.load_config(require_pin=False)
cfg["min_special"] = int(count)
self.save_config(cfg)

View File

@@ -976,6 +976,7 @@ class PasswordManager:
encryption_manager=self.encryption_manager,
parent_seed=self.parent_seed,
bip85=self.bip85,
policy=self.config_manager.get_password_policy(),
)
# Load relay configuration and initialize NostrClient

View File

@@ -21,6 +21,7 @@ import random
import traceback
import base64
from typing import Optional
from dataclasses import dataclass
from termcolor import colored
from pathlib import Path
import shutil
@@ -48,6 +49,16 @@ from password_manager.encryption import EncryptionManager
logger = logging.getLogger(__name__)
@dataclass
class PasswordPolicy:
"""Minimum complexity requirements for generated passwords."""
min_uppercase: int = 2
min_lowercase: int = 2
min_digits: int = 2
min_special: int = 2
class PasswordGenerator:
"""
PasswordGenerator Class
@@ -58,7 +69,11 @@ class PasswordGenerator:
"""
def __init__(
self, encryption_manager: EncryptionManager, parent_seed: str, bip85: BIP85
self,
encryption_manager: EncryptionManager,
parent_seed: str,
bip85: BIP85,
policy: PasswordPolicy | None = None,
):
"""
Initializes the PasswordGenerator with the encryption manager, parent seed, and BIP85 instance.
@@ -72,6 +87,7 @@ class PasswordGenerator:
self.encryption_manager = encryption_manager
self.parent_seed = parent_seed
self.bip85 = bip85
self.policy = policy or PasswordPolicy()
# Derive seed bytes from parent_seed using BIP39 (handled by EncryptionManager)
self.seed_bytes = self.encryption_manager.derive_seed_from_mnemonic(
@@ -224,11 +240,11 @@ class PasswordGenerator:
f"Current character counts - Upper: {current_upper}, Lower: {current_lower}, Digits: {current_digits}, Special: {current_special}"
)
# Set minimum counts
min_upper = 2
min_lower = 2
min_digits = 2
min_special = 2
# Set minimum counts from policy
min_upper = self.policy.min_uppercase
min_lower = self.policy.min_lowercase
min_digits = self.policy.min_digits
min_special = self.policy.min_special
# Initialize derived key index
dk_index = 0

View File

@@ -464,6 +464,10 @@ def config_set(ctx: typer.Context, key: str, value: str) -> None:
"kdf_iterations": lambda v: cfg.set_kdf_iterations(int(v)),
"kdf_mode": lambda v: cfg.set_kdf_mode(v),
"backup_interval": lambda v: cfg.set_backup_interval(float(v)),
"min_uppercase": lambda v: cfg.set_min_uppercase(int(v)),
"min_lowercase": lambda v: cfg.set_min_lowercase(int(v)),
"min_digits": lambda v: cfg.set_min_digits(int(v)),
"min_special": lambda v: cfg.set_min_special(int(v)),
}
action = mapping.get(key)

View File

@@ -0,0 +1,68 @@
import string
from pathlib import Path
import sys
sys.path.append(str(Path(__file__).resolve().parents[1]))
from password_manager.password_generation import PasswordGenerator, PasswordPolicy
class DummyEnc:
def derive_seed_from_mnemonic(self, mnemonic):
return b"\x00" * 32
class DummyBIP85:
def derive_entropy(self, index: int, bytes_len: int, app_no: int = 32) -> bytes:
return bytes((index + i) % 256 for i in range(bytes_len))
def make_generator(policy=None):
pg = PasswordGenerator.__new__(PasswordGenerator)
pg.encryption_manager = DummyEnc()
pg.bip85 = DummyBIP85()
pg.policy = policy or PasswordPolicy()
return pg
def count_types(pw: str):
return (
sum(c.isupper() for c in pw),
sum(c.islower() for c in pw),
sum(c.isdigit() for c in pw),
sum(c in string.punctuation for c in pw),
)
def test_zero_policy_preserves_length():
policy = PasswordPolicy(0, 0, 0, 0)
pg = make_generator(policy)
alphabet = string.ascii_lowercase
dk = bytes(range(32))
result = pg._enforce_complexity("a" * 32, alphabet, dk)
assert len(result) == 32
def test_custom_policy_applied():
policy = PasswordPolicy(
min_uppercase=4, min_lowercase=1, min_digits=3, min_special=2
)
pg = make_generator(policy)
alphabet = string.ascii_letters + string.digits + string.punctuation
dk = bytes(range(32))
result = pg._enforce_complexity("a" * 32, alphabet, dk)
counts = count_types(result)
assert counts[0] >= 4
assert counts[1] >= 1
assert counts[2] >= 3
assert counts[3] >= 2
def test_generate_password_respects_policy():
policy = PasswordPolicy(
min_uppercase=3, min_lowercase=3, min_digits=3, min_special=3
)
pg = make_generator(policy)
pw = pg.generate_password(length=16, index=1)
counts = count_types(pw)
assert all(c >= 3 for c in counts)

View File

@@ -1,5 +1,5 @@
import string
from password_manager.password_generation import PasswordGenerator
from password_manager.password_generation import PasswordGenerator, PasswordPolicy
class DummyEnc:
@@ -16,6 +16,7 @@ def make_generator():
pg = PasswordGenerator.__new__(PasswordGenerator)
pg.encryption_manager = DummyEnc()
pg.bip85 = DummyBIP85()
pg.policy = PasswordPolicy()
return pg

View File

@@ -4,7 +4,7 @@ import sys
sys.path.append(str(Path(__file__).resolve().parents[1]))
from password_manager.password_generation import PasswordGenerator
from password_manager.password_generation import PasswordGenerator, PasswordPolicy
from constants import MIN_PASSWORD_LENGTH
@@ -22,6 +22,7 @@ def make_generator():
pg = PasswordGenerator.__new__(PasswordGenerator)
pg.encryption_manager = DummyEnc()
pg.bip85 = DummyBIP85()
pg.policy = PasswordPolicy()
return pg

View File

@@ -5,7 +5,7 @@ from hypothesis import given, strategies as st, settings
sys.path.append(str(Path(__file__).resolve().parents[1]))
from password_manager.password_generation import PasswordGenerator
from password_manager.password_generation import PasswordGenerator, PasswordPolicy
from password_manager.entry_types import EntryType
@@ -23,6 +23,7 @@ def make_generator():
pg = PasswordGenerator.__new__(PasswordGenerator)
pg.encryption_manager = DummyEnc()
pg.bip85 = DummyBIP85()
pg.policy = PasswordPolicy()
return pg