""" vault.py — Cifrado de credenciales por tenant. Cada tenant tiene sus credenciales cifradas con una clave derivada de: FERNET_KEY (global, del .env) + str(chat_id) (único por tenant) Esto asegura que las credenciales de un tenant no se pueden descifrar con solo la FERNET_KEY — hace falta también el chat_id. """ from __future__ import annotations import base64 import hashlib import json import structlog from cryptography.fernet import Fernet, InvalidToken from utils.sanitize import scrub logger = structlog.get_logger(__name__) def _derive_key(fernet_key: str, chat_id: int) -> bytes: """Deriva una clave Fernet única por tenant usando SHA-256(fernet_key + chat_id).""" raw = (fernet_key + str(chat_id)).encode() digest = hashlib.sha256(raw).digest() return base64.urlsafe_b64encode(digest) def cifrar_credenciales(chat_id: int, usuario: str, password: str, fernet_key: str) -> str: """Retorna JSON cifrado con Fernet listo para guardar en DB.""" key = _derive_key(fernet_key, chat_id) f = Fernet(key) payload = json.dumps({"usuario": usuario, "password": password}).encode() encrypted = f.encrypt(payload).decode() logger.info("credenciales_cifradas", chat_id=chat_id) return encrypted def descifrar_credenciales(chat_id: int, credentials_enc: str, fernet_key: str) -> tuple[str, str]: """ Retorna (usuario, password). Levanta ValueError si no se puede descifrar (clave incorrecta o datos corruptos). """ key = _derive_key(fernet_key, chat_id) f = Fernet(key) try: payload = json.loads(f.decrypt(credentials_enc.encode()).decode()) return payload["usuario"], payload["password"] except (InvalidToken, KeyError, ValueError) as e: logger.error("error_descifrar_credenciales", chat_id=chat_id, error=scrub(str(e))) raise ValueError( f"No se pudieron descifrar credenciales para chat_id={chat_id}" ) from e