mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
Add password policy options to API
This commit is contained in:
@@ -16,6 +16,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||||||
|
|
||||||
from seedpass.core.manager import PasswordManager
|
from seedpass.core.manager import PasswordManager
|
||||||
from seedpass.core.entry_types import EntryType
|
from seedpass.core.entry_types import EntryType
|
||||||
|
from seedpass.core.api import UtilityService
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
@@ -117,11 +118,23 @@ def create_entry(
|
|||||||
etype = (entry.get("type") or entry.get("kind") or "password").lower()
|
etype = (entry.get("type") or entry.get("kind") or "password").lower()
|
||||||
|
|
||||||
if etype == "password":
|
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(
|
index = _pm.entry_manager.add_entry(
|
||||||
entry.get("label"),
|
entry.get("label"),
|
||||||
int(entry.get("length", 12)),
|
int(entry.get("length", 12)),
|
||||||
entry.get("username"),
|
entry.get("username"),
|
||||||
entry.get("url"),
|
entry.get("url"),
|
||||||
|
**kwargs,
|
||||||
)
|
)
|
||||||
return {"id": index}
|
return {"id": index}
|
||||||
|
|
||||||
@@ -566,6 +579,30 @@ def change_password(
|
|||||||
return {"status": "ok"}
|
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")
|
@app.post("/api/v1/vault/lock")
|
||||||
def lock_vault(authorization: str | None = Header(None)) -> dict[str, str]:
|
def lock_vault(authorization: str | None = Header(None)) -> dict[str, str]:
|
||||||
"""Lock the vault and clear sensitive data from memory."""
|
"""Lock the vault and clear sensitive data from memory."""
|
||||||
|
@@ -84,6 +84,34 @@ class SyncResponse(BaseModel):
|
|||||||
delta_ids: List[str] = []
|
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:
|
class VaultService:
|
||||||
"""Thread-safe wrapper around vault operations."""
|
"""Thread-safe wrapper around vault operations."""
|
||||||
|
|
||||||
|
@@ -5,6 +5,8 @@ import pytest
|
|||||||
from seedpass import api
|
from seedpass import api
|
||||||
from test_api import client
|
from test_api import client
|
||||||
from helpers import dummy_nostr_client
|
from helpers import dummy_nostr_client
|
||||||
|
import string
|
||||||
|
from seedpass.core.password_generation import PasswordGenerator, PasswordPolicy
|
||||||
from nostr.client import NostrClient, DEFAULT_RELAYS
|
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 res.status_code == 200
|
||||||
assert called.get("init") is True
|
assert called.get("init") is True
|
||||||
assert api._pm.nostr_client.relays == list(DEFAULT_RELAYS)
|
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)
|
||||||
|
Reference in New Issue
Block a user