Add password policy options to API

This commit is contained in:
thePR0M3TH3AN
2025-07-30 21:23:18 -04:00
parent b76112f11b
commit 6d6a284096
3 changed files with 119 additions and 0 deletions

View File

@@ -16,6 +16,7 @@ from fastapi.middleware.cors import CORSMiddleware
from seedpass.core.manager import PasswordManager
from seedpass.core.entry_types import EntryType
from seedpass.core.api import UtilityService
app = FastAPI()
@@ -117,11 +118,23 @@ def create_entry(
etype = (entry.get("type") or entry.get("kind") or "password").lower()
if etype == "password":
policy_keys = [
"include_special_chars",
"allowed_special_chars",
"special_mode",
"exclude_ambiguous",
"min_uppercase",
"min_lowercase",
"min_digits",
"min_special",
]
kwargs = {k: entry.get(k) for k in policy_keys if entry.get(k) is not None}
index = _pm.entry_manager.add_entry(
entry.get("label"),
int(entry.get("length", 12)),
entry.get("username"),
entry.get("url"),
**kwargs,
)
return {"id": index}
@@ -566,6 +579,30 @@ def change_password(
return {"status": "ok"}
@app.post("/api/v1/password")
def generate_password(
data: dict, authorization: str | None = Header(None)
) -> dict[str, str]:
"""Generate a password using optional policy overrides."""
_check_token(authorization)
assert _pm is not None
length = int(data.get("length", 12))
policy_keys = [
"include_special_chars",
"allowed_special_chars",
"special_mode",
"exclude_ambiguous",
"min_uppercase",
"min_lowercase",
"min_digits",
"min_special",
]
kwargs = {k: data.get(k) for k in policy_keys if data.get(k) is not None}
util = UtilityService(_pm)
password = util.generate_password(length, **kwargs)
return {"password": password}
@app.post("/api/v1/vault/lock")
def lock_vault(authorization: str | None = Header(None)) -> dict[str, str]:
"""Lock the vault and clear sensitive data from memory."""

View File

@@ -84,6 +84,34 @@ class SyncResponse(BaseModel):
delta_ids: List[str] = []
class PasswordPolicyOptions(BaseModel):
"""Optional password policy overrides."""
include_special_chars: bool | None = None
allowed_special_chars: str | None = None
special_mode: str | None = None
exclude_ambiguous: bool | None = None
min_uppercase: int | None = None
min_lowercase: int | None = None
min_digits: int | None = None
min_special: int | None = None
class AddPasswordEntryRequest(PasswordPolicyOptions):
label: str
length: int
username: str | None = None
url: str | None = None
class GeneratePasswordRequest(PasswordPolicyOptions):
length: int
class GeneratePasswordResponse(BaseModel):
password: str
class VaultService:
"""Thread-safe wrapper around vault operations."""

View File

@@ -5,6 +5,8 @@ import pytest
from seedpass import api
from test_api import client
from helpers import dummy_nostr_client
import string
from seedpass.core.password_generation import PasswordGenerator, PasswordPolicy
from nostr.client import NostrClient, DEFAULT_RELAYS
@@ -401,3 +403,55 @@ def test_relay_management_endpoints(client, dummy_nostr_client, monkeypatch):
assert res.status_code == 200
assert called.get("init") is True
assert api._pm.nostr_client.relays == list(DEFAULT_RELAYS)
def test_generate_password_no_special_chars(client):
cl, token = client
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(range(bytes_len))
api._pm.password_generator = PasswordGenerator(DummyEnc(), "seed", DummyBIP85())
api._pm.parent_seed = "seed"
headers = {"Authorization": f"Bearer {token}"}
res = cl.post(
"/api/v1/password",
json={"length": 16, "include_special_chars": False},
headers=headers,
)
assert res.status_code == 200
pw = res.json()["password"]
assert not any(c in string.punctuation for c in pw)
def test_generate_password_allowed_chars(client):
cl, token = client
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))
api._pm.password_generator = PasswordGenerator(DummyEnc(), "seed", DummyBIP85())
api._pm.parent_seed = "seed"
headers = {"Authorization": f"Bearer {token}"}
allowed = "@$"
res = cl.post(
"/api/v1/password",
json={"length": 16, "allowed_special_chars": allowed},
headers=headers,
)
assert res.status_code == 200
pw = res.json()["password"]
specials = [c for c in pw if c in string.punctuation]
assert specials and all(c in allowed for c in specials)