"""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.")