diff --git a/routstr/core/admin.py b/routstr/core/admin.py index 7823fd99..02b81611 100644 --- a/routstr/core/admin.py +++ b/routstr/core/admin.py @@ -343,11 +343,14 @@ async def admin_login( ) -> dict[str, object]: async with create_session() as session: secret = await get_secret(session) + # Read the hash while the session is open; the ORM object is detached + # once the context exits and its attributes can no longer be loaded. + password_hash = secret.admin_password_hash - if not secret.admin_password_hash: + if not password_hash: raise HTTPException(status_code=500, detail="Admin password not configured") - if not vault.verify_password(payload.password, secret.admin_password_hash): + if not vault.verify_password(payload.password, password_hash): raise HTTPException(status_code=401, detail="Invalid password") token = secrets.token_urlsafe(32) @@ -526,7 +529,6 @@ class ModelCreate(BaseModel): async def upsert_provider_model( provider_id: str, payload: ModelCreate ) -> dict[str, object]: - print(payload) logger.info( f"UPSERT_PROVIDER_MODEL called: provider_id={provider_id}, model_id={payload.id}" ) diff --git a/routstr/core/settings.py b/routstr/core/settings.py index 623cc11a..5b9793ee 100644 --- a/routstr/core/settings.py +++ b/routstr/core/settings.py @@ -467,9 +467,6 @@ async def bootstrap_secrets(db_session: AsyncSession) -> None: from . import vault from .db import get_secret - from .logging import get_logger - - logger = get_logger(__name__) raw_blob = await _read_raw_settings_blob(db_session) secret = await get_secret(db_session) @@ -486,12 +483,14 @@ async def bootstrap_secrets(db_session: AsyncSession) -> None: generated = secrets.token_urlsafe(24) secret.admin_password_hash = vault.hash_password(generated) admin_url = (settings.http_url or "http://localhost:8000").rstrip("/") - logger.warning( + # Print to stdout rather than the logger: the operator must see this + # once (e.g. `docker compose logs`), but it must not be persisted + # into the on-disk log files the logger also writes to. + print( "No admin password set; generated a temporary one (shown only " - "now): %s\nLog in at %s/admin and change it from the dashboard " - "settings.", - generated, - admin_url, + f"now): {generated}\nLog in at {admin_url}/admin and change it " + "from the dashboard settings.", + flush=True, ) changed = True diff --git a/routstr/core/vault.py b/routstr/core/vault.py index 1f39da91..8bc04ff3 100644 --- a/routstr/core/vault.py +++ b/routstr/core/vault.py @@ -116,14 +116,20 @@ def verify_password(password: str, stored: str) -> bool: scheme, n, r, p, salt_b64, hash_b64 = stored.split(":") if scheme != "scrypt": return False + n_int, r_int, p_int = int(n), int(r), int(p) + # Cap the work factor at the parameters this module emits. scrypt's + # memory cost grows with N*r, so an oversized N/r in a tampered or + # corrupt stored hash could turn a single login into an OOM/DoS. + if n_int > _SCRYPT_N or r_int > _SCRYPT_R or p_int > _SCRYPT_P: + return False salt = base64.b64decode(salt_b64) expected = base64.b64decode(hash_b64) derived = hashlib.scrypt( password.encode(), salt=salt, - n=int(n), - r=int(r), - p=int(p), + n=n_int, + r=r_int, + p=p_int, dklen=len(expected), ) except (ValueError, TypeError):