"""Симметричное шифрование секретов, хранимых в БД. Ключи лицензий Checkbox и токены CRM попадают в таблицы. В открытом виде они там лежать не должны: дамп базы не обязан давать возможность пробивать чеки от лица клиента. Используется Fernet (AES-128-CBC + HMAC-SHA256) из `cryptography`. """ from __future__ import annotations from functools import lru_cache from cryptography.fernet import Fernet, InvalidToken from app.core.config import settings class DecryptionError(Exception): """Значение не расшифровывается — как правило, сменился ENCRYPTION_KEY.""" @lru_cache def _fernet() -> Fernet: try: return Fernet(settings.encryption_key.encode()) except (ValueError, TypeError) as exc: raise RuntimeError( "ENCRYPTION_KEY некорректен. Сгенерируйте валидный ключ: " "python -c \"from cryptography.fernet import Fernet; " 'print(Fernet.generate_key().decode())"' ) from exc def encrypt(value: str) -> str: return _fernet().encrypt(value.encode()).decode() def decrypt(value: str) -> str: try: return _fernet().decrypt(value.encode()).decode() except InvalidToken as exc: raise DecryptionError( "Не удалось расшифровать значение. Вероятная причина — ENCRYPTION_KEY " "изменился с момента сохранения. Секрет нужно ввести заново." ) from exc def mask(value: str, visible: int = 4) -> str: """Маска для показа секрета в интерфейсе: `****ab12`. Наружу секреты отдаются только в таком виде — расшифрованное значение не покидает бэкенд. """ if not value: return "" if len(value) <= visible: return "*" * len(value) return "*" * (len(value) - visible) + value[-visible:]