Files
webproxyserver-proxyserver/app/auth.py
T
2026-10-05 13:33:59 +02:00

105 lines
3.2 KiB
Python

"""Passwörter, Benutzer-Cache und Brute-Force-Schutz für die Proxy-Anmeldung."""
import asyncio
import hashlib
import hmac
import os
import secrets
import time
from .db import db
ITERATIONS = 200_000
def hash_password(password: str) -> str:
salt = os.urandom(16)
dk = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, ITERATIONS)
return f"pbkdf2_sha256${ITERATIONS}${salt.hex()}${dk.hex()}"
def verify_password(password: str, stored: str) -> bool:
try:
algo, iters, salt, digest = stored.split("$")
if algo != "pbkdf2_sha256":
return False
dk = hashlib.pbkdf2_hmac("sha256", password.encode(), bytes.fromhex(salt), int(iters))
return hmac.compare_digest(dk.hex(), digest)
except (ValueError, TypeError):
return False
def new_token() -> str:
return secrets.token_urlsafe(32)
def token_hash(token: str) -> str:
return hashlib.sha256(token.encode()).hexdigest()
class UserCache:
"""Hält Benutzer im Speicher und merkt sich erfolgreiche Anmeldungen,
damit nicht jede Proxy-Verbindung eine teure PBKDF2-Prüfung auslöst."""
VERIFIED_TTL = 600
FAIL_WINDOW = 300
FAIL_LIMIT = 10
def __init__(self):
self.users: dict[str, dict] = {}
self.verified: dict[tuple, float] = {}
self.failures: dict[str, list[float]] = {}
self.last_seen: dict[int, float] = {}
self.reload()
def reload(self) -> None:
rows = db.query("SELECT * FROM users")
self.users = {r["username"].lower(): r for r in rows}
self.verified.clear()
def get(self, username: str) -> dict | None:
return self.users.get(username.lower())
def is_locked(self, ip: str) -> bool:
now = time.time()
fails = [t for t in self.failures.get(ip, []) if now - t < self.FAIL_WINDOW]
self.failures[ip] = fails
return len(fails) >= self.FAIL_LIMIT
def _fail(self, ip: str) -> None:
self.failures.setdefault(ip, []).append(time.time())
async def authenticate(self, username: str, password: str, ip: str) -> dict | None:
if self.is_locked(ip):
return None
user = self.get(username)
if not user or not user["enabled"]:
self._fail(ip)
return None
key = (user["id"], hashlib.sha256((password + user["password_hash"]).encode()).digest())
exp = self.verified.get(key)
now = time.time()
if exp and exp > now:
self.touch(user["id"])
return user
ok = await asyncio.to_thread(verify_password, password, user["password_hash"])
if not ok:
self._fail(ip)
return None
if len(self.verified) > 5000:
self.verified.clear()
self.verified[key] = now + self.VERIFIED_TTL
self.touch(user["id"])
return user
def touch(self, user_id: int) -> None:
self.last_seen[user_id] = time.time()
def flush_last_seen(self) -> None:
if not self.last_seen:
return
items, self.last_seen = list(self.last_seen.items()), {}
db.executemany("UPDATE users SET last_seen=? WHERE id=?", [(t, uid) for uid, t in items])
users = UserCache()