shinkan-jinkendo/backend/media_grant.py
Lars 32b28a93f3
Some checks failed
Deploy Development / deploy (push) Successful in 49s
Test Suite / pytest-backend (push) Successful in 47s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 15s
Test Suite / k6 /health Baseline (push) Successful in 35s
Test Suite / playwright-tests (push) Failing after 5m43s
Harden login, sessions, and media access before the dev deploy.
Limit brute-force attempts, require admin TOTP, and keep session tokens out of media URLs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 07:45:53 +02:00

50 lines
1.6 KiB
Python

"""Kurzlebige Medien-Tokens. Kein Sitzungstoken — nur für Dateiabruf per Query."""
from __future__ import annotations
import hashlib
import hmac
import os
import time
from typing import Optional
MEDIA_GRANT_TTL_SECONDS = 600
_PREFIX = "mg1"
def _key() -> bytes:
dedicated = (os.getenv("MEDIA_TOKEN_SECRET") or "").strip()
if dedicated:
return hashlib.sha256(dedicated.encode("utf-8")).digest()
material = (os.getenv("DB_PASSWORD") or "shinkan-dev-media").encode("utf-8")
return hashlib.sha256(b"shinkan-media-grant-v1|" + material).digest()
def issue_media_grant(profile_id: int, *, ttl_seconds: int = MEDIA_GRANT_TTL_SECONDS) -> tuple[str, int]:
exp = int(time.time()) + int(ttl_seconds)
payload = f"{int(profile_id)}.{exp}"
sig = hmac.new(_key(), payload.encode("ascii"), hashlib.sha256).hexdigest()
return f"{_PREFIX}.{payload}.{sig}", exp
def profile_id_from_media_grant(token: Optional[str], *, now: Optional[int] = None) -> Optional[int]:
if not token or not token.startswith(f"{_PREFIX}."):
return None
parts = token.split(".")
if len(parts) != 4:
return None
_, pid_s, exp_s, sig = parts
if not pid_s.isdigit() or not exp_s.isdigit():
return None
payload = f"{pid_s}.{exp_s}"
expected = hmac.new(_key(), payload.encode("ascii"), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
return None
exp = int(exp_s)
moment = int(time.time() if now is None else now)
if exp < moment:
return None
pid = int(pid_s)
if pid < 1:
return None
return pid