bifroest/app/identity/passwords.py
2026-10-27 08:34:17 +01:00

50 lines
1.7 KiB
Python

"""Passwörter mit Argon2id (Übergangslösung bis Keycloak, → Konzept 5.1).
Die Datenbank kennt nur den Hash. Einmal-Passwörter sieht der Admin genau einmal.
"""
import secrets
from argon2 import PasswordHasher
from argon2.exceptions import InvalidHashError, VerificationError
from app.core.errors import Invalid
# Standardprofil von argon2-cffi (RFC 9106, Argon2id)
HASHER = PasswordHasher()
# Für unbekannte Nutzer wird gegen diesen Hash geprüft, damit die Antwortzeit nichts verrät
_DUMMY = HASHER.hash(secrets.token_urlsafe(16))
MIN_LENGTH = 12
def hash_password(password: str) -> str:
return HASHER.hash(password)
def verify(password_hash: str | None, password: str) -> bool:
try:
return HASHER.verify(password_hash or _DUMMY, password) and password_hash is not None
except (VerificationError, InvalidHashError):
return False
def needs_rehash(password_hash: str) -> bool:
return HASHER.check_needs_rehash(password_hash)
def one_time() -> str:
"""Gut abtippbar: ohne leicht verwechselbare Zeichen, vier Gruppen."""
alphabet = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"
return "-".join("".join(secrets.choice(alphabet) for _ in range(4)) for _ in range(4))
def check_policy(password: str, username: str) -> None:
if len(password) < MIN_LENGTH:
raise Invalid(f"Das Passwort braucht mindestens {MIN_LENGTH} Zeichen.")
if len(password) > 200:
raise Invalid("Das Passwort ist zu lang (höchstens 200 Zeichen).")
if username.lower() in password.lower():
raise Invalid("Das Passwort darf den Nutzernamen nicht enthalten.")
if len(set(password)) < 5:
raise Invalid("Das Passwort ist zu einfach.")