from __future__ import annotations import hashlib import hmac import os from dataclasses import dataclass from typing import Optional _ITER = 200_000 @dataclass class User: username: str role: str salt_hex: str hash_hex: str def hash_password(password: str, salt: Optional[bytes] = None) -> tuple[str, str]: salt = salt if salt is not None else os.urandom(16) dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, _ITER) return salt.hex(), dk.hex() def verify_password(password: str, salt_hex: str, hash_hex: str) -> bool: try: dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), bytes.fromhex(salt_hex), _ITER) except ValueError: return False return hmac.compare_digest(dk.hex(), hash_hex) def parse_users(raw: str) -> dict[str, User]: users: dict[str, User] = {} for entry in raw.replace("\n", ",").split(","): entry = entry.strip() if not entry: continue parts = entry.split(":") if len(parts) != 3 or "$" not in parts[2]: continue # skip malformed; never crash the auth gate username, role, sh = parts salt_hex, hash_hex = sh.split("$", 1) users[username.strip()] = User(username.strip(), role.strip(), salt_hex, hash_hex) return users def make_auth_fn(users: dict[str, User]): def _auth(username: str, password: str) -> bool: u = users.get(username) if u is None: verify_password(password, "00" * 16, "00") # reduce timing signal for unknown users return False return verify_password(password, u.salt_hex, u.hash_hex) return _auth def role_of(users: dict[str, User], username: str) -> Optional[str]: u = users.get(username) return u.role if u else None