105 lines
3.2 KiB
Python
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()
|