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:
@@ -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()
|
||||
+3
-1
@@ -20,10 +20,12 @@ class Player(Base):
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
username: Mapped[str] = mapped_column(String(40), unique=True, index=True)
|
||||
totp_secret: Mapped[str] = mapped_column(String(32))
|
||||
# Sifrovany cez db/crypto.py (Fernet) -- nikdy neuklada plaintext secret.
|
||||
totp_secret: Mapped[str] = mapped_column(String(255))
|
||||
# Posledny pouzity TOTP casovy krok -- ochrana proti replay v ramci okna.
|
||||
totp_last_step: Mapped[int] = mapped_column(Integer, default=0)
|
||||
# Session token pre auto-reconnect (poslany v Socket.IO `auth`).
|
||||
# Uklada sa SHA-256 hash (db/crypto.hash_token), nie surovy token.
|
||||
auth_token: Mapped[str | None] = mapped_column(
|
||||
String(64), unique=True, nullable=True
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user