""" Zentrale Krypto-Hilfsfunktionen für TimeMaster. Verwendet Fernet-Verschlüsselung (AES-128-CBC + HMAC-SHA256). Der Schlüssel wird per SHA-256 abgeleitet aus: - SECRET_KEY_DATA (empfohlen, separater Key für Datenverschlüsselung) - SECRET_KEY (Fallback wenn SECRET_KEY_DATA nicht gesetzt – Warnung beim Start) Verwendung: from app.core.crypto import encrypt_value, decrypt_value stored = encrypt_value("geheimes-passwort") plain = decrypt_value(stored) """ from __future__ import annotations import base64 import hashlib import logging from cryptography.fernet import Fernet, InvalidToken from app.core.config import settings logger = logging.getLogger(__name__) def get_fernet_key() -> bytes: """Gibt den Fernet-Key zurück. Bevorzugt SECRET_KEY_DATA (separater Datenschlüssel). Fällt auf SECRET_KEY zurück wenn SECRET_KEY_DATA nicht gesetzt ist, und gibt dabei eine Warnung aus (JWT- und Datenschlüssel identisch). Der Key wird per SHA-256 auf 32 Bytes normiert und dann base64url-kodiert. """ if settings.secret_key_data: key_material = settings.secret_key_data else: logger.warning( "SECRET_KEY_DATA nicht gesetzt — JWT-Key wird auch für Datenverschlüsselung " "verwendet. Bitte SECRET_KEY_DATA in .env setzen für verbesserte Sicherheit." ) key_material = settings.secret_key key_bytes = hashlib.sha256(key_material.encode()).digest() return base64.urlsafe_b64encode(key_bytes) def _fernet() -> Fernet: """Erstellt eine Fernet-Instanz aus dem konfigurierten Datenschlüssel.""" return Fernet(get_fernet_key()) def encrypt_value(plain: str) -> str: """Verschlüsselt einen Klartext-String per Fernet. Gibt den chiffrierten String zurück.""" return _fernet().encrypt(plain.encode()).decode() def decrypt_value(encrypted: str) -> str: """ Entschlüsselt einen Fernet-verschlüsselten String. Wirft ValueError bei ungültigem Token oder falschem Schlüssel. """ try: return _fernet().decrypt(encrypted.encode()).decode() except InvalidToken as exc: raise ValueError("Entschlüsselung fehlgeschlagen – ungültiger Token oder falscher Schlüssel.") from exc