Add login lockout and encrypt Player.totp_secret/auth_token at rest

Per-username lockout (5 failed TOTP attempts / 5 min) stops account-targeted
brute force regardless of source IP. Player.totp_secret is now Fernet-
encrypted (ENCRYPTION_KEY env, db/crypto.py) instead of stored in plaintext,
and auth_token is stored as a SHA-256 hash rather than the raw session token.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
tim
2026-07-01 19:49:56 +02:00
co-authored by Claude Sonnet 5
parent 0845562a21
commit fbe0c3aa18
7 changed files with 177 additions and 12 deletions
+42
View File
@@ -0,0 +1,42 @@
"""Sifrovanie/hashovanie citlivych stlpcov (Player.totp_secret, Player.auth_token).
totp_secret sa musi dat spatne desifrovat (treba ho na vygenerovanie/overenie
TOTP kodu), preto Fernet -- symetricke sifrovanie s klucom z env ENCRYPTION_KEY.
auth_token sa iba porovnava, nikdy nepotrebujeme povodnu hodnotu spat, preto
staci jednosmerny SHA-256 hash (token ma 384 bitov entropie z
secrets.token_urlsafe(48) v api/auth.py, takze netreba salt/pepper).
"""
import hashlib
import os
from cryptography.fernet import Fernet, InvalidToken
_ENV_VAR = "ENCRYPTION_KEY"
def _fernet() -> Fernet:
key = os.environ.get(_ENV_VAR)
if not key:
raise RuntimeError(
f"{_ENV_VAR} nie je nastaveny. Vygeneruj ho pomocou:\n"
' python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"\n'
"a nastav ako env premennu (drz ho stabilny -- zmena znamena, "
"ze existujuce totp_secret sa uz nedaju desifrovat)."
)
return Fernet(key.encode())
def encrypt(plaintext: str) -> str:
return _fernet().encrypt(plaintext.encode()).decode()
def decrypt(ciphertext: str) -> str:
try:
return _fernet().decrypt(ciphertext.encode()).decode()
except InvalidToken as exc:
raise ValueError("Neplatny alebo poskodeny sifrovany udaj.") from exc
def hash_token(token: str) -> str:
return hashlib.sha256(token.encode()).hexdigest()