treefmt / nix fmt (pull_request) Successful in 6s
test ebook search / test-ebook-search (pull_request) Successful in 39s
pytest / pytest (pull_request) Successful in 40s
build_systems / build-brain (pull_request) Successful in 1m0s
build_systems / build-bob (pull_request) Successful in 1m2s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m33s
build_systems / build-jeeves (pull_request) Successful in 2m25s
test ebook search / test-ebook-search (push) Successful in 35s
build_systems / build-brain (push) Successful in 37s
build_systems / build-bob (push) Successful in 37s
build_systems / build-jeeves (push) Successful in 2m17s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 29s
build_systems / build-rhapsody-in-green (push) Successful in 49s
- implement FastAPI and HTMX lobby and game interfaces - support up to four human and AI-controlled players - add configurable rules, victory conditions, and expansion modules - support validated JSON cards, patrons, objectives, and outposts
41 lines
1.5 KiB
Python
41 lines
1.5 KiB
Python
"""Opaque browser credentials and CSRF protection."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import hmac
|
|
import secrets
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
|
|
class CredentialService:
|
|
"""Hash browser credentials with a stable per-installation key."""
|
|
|
|
def __init__(self, key_path: Path) -> None:
|
|
"""Load or create the installation's credential-signing key."""
|
|
key_path.parent.mkdir(parents=True, exist_ok=True)
|
|
if not key_path.exists():
|
|
key_path.write_bytes(secrets.token_bytes(32))
|
|
key_path.chmod(0o600)
|
|
self._key = key_path.read_bytes()
|
|
|
|
@staticmethod
|
|
def issue() -> str:
|
|
"""Issue a cryptographically random browser credential."""
|
|
return secrets.token_urlsafe(32)
|
|
|
|
def digest(self, credential: str) -> str:
|
|
"""Create the persistent keyed digest of a browser credential."""
|
|
return hmac.new(self._key, credential.encode(), hashlib.sha256).hexdigest()
|
|
|
|
def csrf(self, credential: str, room_code: str) -> str:
|
|
"""Create a room-scoped CSRF token for a browser credential."""
|
|
return hmac.new(self._key, f"csrf:{credential}:{room_code}".encode(), hashlib.sha256).hexdigest()
|
|
|
|
def valid_csrf(self, credential: str, room_code: str, candidate: str) -> bool:
|
|
"""Validate a candidate CSRF token using constant-time comparison."""
|
|
return hmac.compare_digest(self.csrf(credential, room_code), candidate)
|