feat(creds): livrare creds per-cerere la worker (criptat efemer + sesiuni per-cont)
Plan sect.5: parola RAR vine per-cerere, stocata CRIPTATA in submission pana la primul login reusit pe cont, apoi stearsa; JWT 30h acopera restul. - app/crypto.py: Fernet, cheie din AUTOPASS_creds_key (nesetata -> efemera la runtime, creds nu supravietuiesc restartului). encrypt/decrypt_creds. - schema + migrare: submissions.rar_creds_enc (creds criptate). - ingestie: cripteaza rar_credentials, le lipeste de fiecare submission nou. Niciodata in clar in DB. - worker: AccountSessions (login per-cont cu creds decriptate, cache JWT in memorie, sterge creds-urile contului dupa primul login + refresh nomenclator). 401 creds gresite -> error fara retry; token expirat -> invalidare + requeue; fara creds (restart) -> requeue "indisponibile" (ROAAUTO re-trimite). claim_one intoarce account_id + creds_enc; recover_orphans filtrabil pe cont. - requirements: cryptography==46.0.5. Nota: refresh nomenclator e acum lazy la primul login per-cont (nu la pornire); seed-ul fallback acopera editorul offline. 10 teste noi (tests/test_creds_delivery.py). 95 pass total. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
57
app/crypto.py
Normal file
57
app/crypto.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""Criptare simetrica pentru credentialele RAR efemere (zero-storage at rest).
|
||||
|
||||
Plan sect. 5: parola RAR vine per-cerere, se stocheaza CRIPTATA in submission
|
||||
pana la primul login reusit pentru cont, apoi se sterge. JWT (30h) acopera
|
||||
restul trimiterilor. Cheia traieste doar in `AUTOPASS_creds_key` (env), niciodata
|
||||
in cod sau in DB.
|
||||
|
||||
Daca `AUTOPASS_creds_key` nu e setat, generam o cheie EFEMERA la prima folosire:
|
||||
creds criptate NU supravietuiesc unui restart (acceptabil — modelul e efemer,
|
||||
ROAAUTO re-trimite). Pentru productie seteaza o cheie persistenta (vezi README/deploy).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from functools import lru_cache
|
||||
|
||||
from cryptography.fernet import Fernet, InvalidToken
|
||||
|
||||
from .config import get_settings
|
||||
|
||||
|
||||
@lru_cache
|
||||
def _fernet() -> Fernet:
|
||||
key = get_settings().creds_key
|
||||
if key:
|
||||
return Fernet(key.encode() if isinstance(key, str) else key)
|
||||
generated = Fernet.generate_key()
|
||||
print(
|
||||
"[crypto] AUTOPASS_creds_key nesetat — cheie efemera generata; creds "
|
||||
"criptate NU supravietuiesc restartului worker-ului/API-ului",
|
||||
flush=True,
|
||||
)
|
||||
return Fernet(generated)
|
||||
|
||||
|
||||
def reset_cache() -> None:
|
||||
"""Reseteaza cheia memorata (pentru teste care schimba env-ul)."""
|
||||
_fernet.cache_clear()
|
||||
|
||||
|
||||
def encrypt_creds(creds: dict) -> str:
|
||||
"""Cripteaza un dict de creds -> token Fernet (str). Compact, fara spatii."""
|
||||
blob = json.dumps(creds, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
||||
return _fernet().encrypt(blob).decode("ascii")
|
||||
|
||||
|
||||
def decrypt_creds(token: str | None) -> dict | None:
|
||||
"""Decripteaza un token Fernet -> dict, sau None daca lipseste/cheie gresita/corupt."""
|
||||
if not token:
|
||||
return None
|
||||
try:
|
||||
plain = _fernet().decrypt(token.encode("ascii"))
|
||||
data = json.loads(plain.decode("utf-8"))
|
||||
return data if isinstance(data, dict) else None
|
||||
except (InvalidToken, ValueError, TypeError):
|
||||
return None
|
||||
Reference in New Issue
Block a user