mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
Add configurable password policy
This commit is contained in:
@@ -171,8 +171,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`, or `relays`.
|
- **`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`.
|
- **`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.
|
- **`seedpass config toggle-secret-mode`** – Interactively enable or disable Secret Mode and set the clipboard delay.
|
||||||
|
|
||||||
### `fingerprint` Commands
|
### `fingerprint` Commands
|
||||||
@@ -209,4 +209,5 @@ 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`, or `secret_mode_enabled` through the `config` commands.
|
- 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 script‑friendly and can be piped into other commands.
|
- `entry get` is script‑friendly and can be piped into other commands.
|
||||||
|
@@ -50,6 +50,10 @@ class ConfigManager:
|
|||||||
"backup_interval": 0,
|
"backup_interval": 0,
|
||||||
"secret_mode_enabled": False,
|
"secret_mode_enabled": False,
|
||||||
"clipboard_clear_delay": 45,
|
"clipboard_clear_delay": 45,
|
||||||
|
"min_uppercase": 2,
|
||||||
|
"min_lowercase": 2,
|
||||||
|
"min_digits": 2,
|
||||||
|
"min_special": 2,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
data = self.vault.load_config()
|
data = self.vault.load_config()
|
||||||
@@ -66,6 +70,10 @@ class ConfigManager:
|
|||||||
data.setdefault("backup_interval", 0)
|
data.setdefault("backup_interval", 0)
|
||||||
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("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
|
# Migrate legacy hashed_password.enc if present and password_hash is missing
|
||||||
legacy_file = self.fingerprint_dir / "hashed_password.enc"
|
legacy_file = self.fingerprint_dir / "hashed_password.enc"
|
||||||
@@ -218,3 +226,36 @@ class ConfigManager:
|
|||||||
"""Retrieve the backup interval in seconds."""
|
"""Retrieve the backup interval in seconds."""
|
||||||
config = self.load_config(require_pin=False)
|
config = self.load_config(require_pin=False)
|
||||||
return float(config.get("backup_interval", 0))
|
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)
|
||||||
|
@@ -976,6 +976,7 @@ class PasswordManager:
|
|||||||
encryption_manager=self.encryption_manager,
|
encryption_manager=self.encryption_manager,
|
||||||
parent_seed=self.parent_seed,
|
parent_seed=self.parent_seed,
|
||||||
bip85=self.bip85,
|
bip85=self.bip85,
|
||||||
|
policy=self.config_manager.get_password_policy(),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Load relay configuration and initialize NostrClient
|
# Load relay configuration and initialize NostrClient
|
||||||
|
@@ -21,6 +21,7 @@ import random
|
|||||||
import traceback
|
import traceback
|
||||||
import base64
|
import base64
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from dataclasses import dataclass
|
||||||
from termcolor import colored
|
from termcolor import colored
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
@@ -48,6 +49,16 @@ from password_manager.encryption import EncryptionManager
|
|||||||
logger = logging.getLogger(__name__)
|
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:
|
class PasswordGenerator:
|
||||||
"""
|
"""
|
||||||
PasswordGenerator Class
|
PasswordGenerator Class
|
||||||
@@ -58,7 +69,11 @@ class PasswordGenerator:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
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.
|
Initializes the PasswordGenerator with the encryption manager, parent seed, and BIP85 instance.
|
||||||
@@ -72,6 +87,7 @@ class PasswordGenerator:
|
|||||||
self.encryption_manager = encryption_manager
|
self.encryption_manager = encryption_manager
|
||||||
self.parent_seed = parent_seed
|
self.parent_seed = parent_seed
|
||||||
self.bip85 = bip85
|
self.bip85 = bip85
|
||||||
|
self.policy = policy or PasswordPolicy()
|
||||||
|
|
||||||
# Derive seed bytes from parent_seed using BIP39 (handled by EncryptionManager)
|
# Derive seed bytes from parent_seed using BIP39 (handled by EncryptionManager)
|
||||||
self.seed_bytes = self.encryption_manager.derive_seed_from_mnemonic(
|
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}"
|
f"Current character counts - Upper: {current_upper}, Lower: {current_lower}, Digits: {current_digits}, Special: {current_special}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Set minimum counts
|
# Set minimum counts from policy
|
||||||
min_upper = 2
|
min_upper = self.policy.min_uppercase
|
||||||
min_lower = 2
|
min_lower = self.policy.min_lowercase
|
||||||
min_digits = 2
|
min_digits = self.policy.min_digits
|
||||||
min_special = 2
|
min_special = self.policy.min_special
|
||||||
|
|
||||||
# Initialize derived key index
|
# Initialize derived key index
|
||||||
dk_index = 0
|
dk_index = 0
|
||||||
|
@@ -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_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)),
|
||||||
|
"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)
|
action = mapping.get(key)
|
||||||
|
68
src/tests/test_password_generation_policy.py
Normal file
68
src/tests/test_password_generation_policy.py
Normal 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)
|
@@ -1,5 +1,5 @@
|
|||||||
import string
|
import string
|
||||||
from password_manager.password_generation import PasswordGenerator
|
from password_manager.password_generation import PasswordGenerator, PasswordPolicy
|
||||||
|
|
||||||
|
|
||||||
class DummyEnc:
|
class DummyEnc:
|
||||||
@@ -16,6 +16,7 @@ def make_generator():
|
|||||||
pg = PasswordGenerator.__new__(PasswordGenerator)
|
pg = PasswordGenerator.__new__(PasswordGenerator)
|
||||||
pg.encryption_manager = DummyEnc()
|
pg.encryption_manager = DummyEnc()
|
||||||
pg.bip85 = DummyBIP85()
|
pg.bip85 = DummyBIP85()
|
||||||
|
pg.policy = PasswordPolicy()
|
||||||
return pg
|
return pg
|
||||||
|
|
||||||
|
|
||||||
|
@@ -4,7 +4,7 @@ import sys
|
|||||||
|
|
||||||
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
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
|
from constants import MIN_PASSWORD_LENGTH
|
||||||
|
|
||||||
|
|
||||||
@@ -22,6 +22,7 @@ def make_generator():
|
|||||||
pg = PasswordGenerator.__new__(PasswordGenerator)
|
pg = PasswordGenerator.__new__(PasswordGenerator)
|
||||||
pg.encryption_manager = DummyEnc()
|
pg.encryption_manager = DummyEnc()
|
||||||
pg.bip85 = DummyBIP85()
|
pg.bip85 = DummyBIP85()
|
||||||
|
pg.policy = PasswordPolicy()
|
||||||
return pg
|
return pg
|
||||||
|
|
||||||
|
|
||||||
|
@@ -5,7 +5,7 @@ from hypothesis import given, strategies as st, settings
|
|||||||
|
|
||||||
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
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
|
from password_manager.entry_types import EntryType
|
||||||
|
|
||||||
|
|
||||||
@@ -23,6 +23,7 @@ def make_generator():
|
|||||||
pg = PasswordGenerator.__new__(PasswordGenerator)
|
pg = PasswordGenerator.__new__(PasswordGenerator)
|
||||||
pg.encryption_manager = DummyEnc()
|
pg.encryption_manager = DummyEnc()
|
||||||
pg.bip85 = DummyBIP85()
|
pg.bip85 = DummyBIP85()
|
||||||
|
pg.policy = PasswordPolicy()
|
||||||
return pg
|
return pg
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user