50 lines
1.7 KiB
Python
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.")
|