Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import hashlib | |
| import hmac | |
| import os | |
| from dataclasses import dataclass | |
| from typing import Optional | |
| _ITER = 200_000 | |
| 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 | |