import hashlib import hmac import secrets from datetime import UTC, datetime, timedelta from argon2 import PasswordHasher from argon2.exceptions import InvalidHashError, VerifyMismatchError from sqlalchemy import select from sqlalchemy.orm import Session from app.models import RateLimit _hasher = PasswordHasher() # Dummy-Hash gegen Timing-Angriffe: bei unbekanntem Konto wird trotzdem # eine Verifikation durchgefuehrt, damit die Antwortzeit gleich bleibt. _DUMMY_HASH = _hasher.hash("dummy-password-for-constant-time-comparison") def hash_password(password: str) -> str: return _hasher.hash(password) def verify_password(password: str, password_hash: str | None) -> bool: try: _hasher.verify(password_hash or _DUMMY_HASH, password) except (VerifyMismatchError, InvalidHashError): return False return password_hash is not None def needs_rehash(password_hash: str) -> bool: try: return _hasher.check_needs_rehash(password_hash) except InvalidHashError: return True def new_token() -> str: """256 Bit Zufall, URL-tauglich.""" return secrets.token_urlsafe(32) def hash_token(token: str) -> str: """Token landen nur als Hash in der Datenbank. Kein Salt noetig - der Eingabewert ist bereits hochentropisch.""" return hashlib.sha256(token.encode()).hexdigest() def tokens_equal(a: str, b: str) -> bool: return hmac.compare_digest(a, b) def utcnow() -> datetime: return datetime.now(UTC).replace(tzinfo=None) def normalize_email(email: str) -> str: return email.strip().lower() def check_rate_limit( db: Session, bucket: str, *, limit: int, window_minutes: int ) -> bool: """True = Anfrage erlaubt. Zaehlt hoch und gibt False zurueck, sobald das Limit im aktuellen Fenster erreicht ist.""" now = utcnow() window = now.replace(second=0, microsecond=0) window = window - timedelta(minutes=window.minute % window_minutes) row = db.get(RateLimit, (bucket[:160], window)) if row is None: row = RateLimit(bucket=bucket[:160], window_start=window, count=1) db.add(row) db.flush() return True if row.count >= limit: return False row.count += 1 db.flush() return True def purge_rate_limits(db: Session, older_than_hours: int = 24) -> None: cutoff = utcnow() - timedelta(hours=older_than_hours) for row in db.scalars( select(RateLimit).where(RateLimit.window_start < cutoff) ).all(): db.delete(row)