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
Limit brute-force attempts, require admin TOTP, and keep session tokens out of media URLs. Co-authored-by: Cursor <cursoragent@cursor.com>
64 lines
2.0 KiB
Python
64 lines
2.0 KiB
Python
"""TOTP (RFC 6238) und einmalige Wiederherstellungscodes — nur Standardbibliothek."""
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import hashlib
|
|
import hmac
|
|
import secrets
|
|
import struct
|
|
import time
|
|
from typing import Optional
|
|
from urllib.parse import quote
|
|
|
|
|
|
def generate_totp_secret() -> str:
|
|
return base64.b32encode(secrets.token_bytes(20)).decode("ascii").rstrip("=")
|
|
|
|
|
|
def _decode_secret(secret: str) -> bytes:
|
|
raw = "".join(ch for ch in (secret or "").upper() if ch.isalnum())
|
|
pad = "=" * ((8 - len(raw) % 8) % 8)
|
|
return base64.b32decode(raw + pad, casefold=True)
|
|
|
|
|
|
def totp_code(secret: str, counter: int) -> str:
|
|
digest = hmac.new(_decode_secret(secret), struct.pack(">Q", counter), hashlib.sha1).digest()
|
|
offset = digest[-1] & 0x0F
|
|
binary = struct.unpack(">I", digest[offset : offset + 4])[0] & 0x7FFFFFFF
|
|
return f"{binary % 1_000_000:06d}"
|
|
|
|
|
|
def verify_totp(secret: Optional[str], code: str, *, window: int = 1, now: Optional[int] = None) -> bool:
|
|
if not secret:
|
|
return False
|
|
digits = "".join(ch for ch in (code or "") if ch.isdigit())
|
|
if len(digits) != 6:
|
|
return False
|
|
moment = int(time.time() if now is None else now)
|
|
counter = moment // 30
|
|
for delta in range(-window, window + 1):
|
|
if hmac.compare_digest(totp_code(secret, counter + delta), digits):
|
|
return True
|
|
return False
|
|
|
|
|
|
def otpauth_uri(*, secret: str, account_name: str, issuer: str = "Shinkan Jinkendo") -> str:
|
|
label = quote(f"{issuer}:{account_name}")
|
|
return (
|
|
f"otpauth://totp/{label}?secret={quote(secret)}"
|
|
f"&issuer={quote(issuer)}&algorithm=SHA1&digits=6&period=30"
|
|
)
|
|
|
|
|
|
def generate_recovery_codes(count: int = 8) -> list[str]:
|
|
codes = []
|
|
for _ in range(count):
|
|
raw = secrets.token_hex(4)
|
|
codes.append(f"{raw[:4]}-{raw[4:]}")
|
|
return codes
|
|
|
|
|
|
def hash_recovery_code(code: str) -> str:
|
|
normalized = "".join(ch for ch in (code or "").lower() if ch.isalnum())
|
|
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
|