from __future__ import annotations import base64 import hashlib import hmac import os import time from http import cookies from app.config import Settings COOKIE_NAME = "tv_dispatcher_session" SESSION_TTL_SECONDS = 60 * 60 * 12 PASSWORD_ITERATIONS = 200_000 def _sign(secret: str, value: str) -> str: return hmac.new(secret.encode(), value.encode(), hashlib.sha256).hexdigest() def make_session_cookie(settings: Settings) -> str: payload = f"{settings.admin_username}:{int(time.time())}" encoded = base64.urlsafe_b64encode(payload.encode()).decode() return f"{encoded}.{_sign(settings.session_secret, encoded)}" def is_valid_session(settings: Settings, cookie_header: str | None) -> bool: if not cookie_header: return False jar = cookies.SimpleCookie(cookie_header) morsel = jar.get(COOKIE_NAME) if not morsel: return False try: encoded, signature = morsel.value.split(".", 1) if not hmac.compare_digest(signature, _sign(settings.session_secret, encoded)): return False raw = base64.urlsafe_b64decode(encoded.encode()).decode() username, issued = raw.rsplit(":", 1) return username == settings.admin_username and time.time() - int(issued) <= SESSION_TTL_SECONDS except Exception: return False def hash_password(password: str) -> str: salt = os.urandom(16) digest = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, PASSWORD_ITERATIONS) return f"pbkdf2_sha256${PASSWORD_ITERATIONS}${base64.b64encode(salt).decode()}${base64.b64encode(digest).decode()}" def verify_password(password: str, password_hash: str) -> bool: try: algorithm, iterations, salt, digest = password_hash.split("$", 3) if algorithm != "pbkdf2_sha256": return False expected = hashlib.pbkdf2_hmac( "sha256", password.encode(), base64.b64decode(salt.encode()), int(iterations), ) return hmac.compare_digest(base64.b64encode(expected).decode(), digest) except Exception: return False def check_credentials(settings: Settings, username: str, password: str, password_hash: str) -> bool: return hmac.compare_digest(username, settings.admin_username) and verify_password(password, password_hash)