Spaces:
Paused
Paused
| """ | |
| Encryption service using Fernet symmetric encryption. | |
| This module provides secure encryption/decryption for secrets stored in the database. | |
| """ | |
| import logging | |
| import os | |
| from cryptography.fernet import Fernet, InvalidToken | |
| from django.conf import settings | |
| logger = logging.getLogger(__name__) | |
| class EncryptionError(Exception): | |
| """Raised when encryption/decryption fails.""" | |
| pass | |
| class EncryptionService: | |
| """ | |
| Provides symmetric encryption/decryption using Fernet. | |
| The encryption key is loaded from the ENCRYPTION_KEY environment variable | |
| or Django settings. If not set, encryption operations will fail. | |
| """ | |
| _fernet = None | |
| def _get_fernet(cls) -> Fernet: | |
| """Get or create the Fernet instance.""" | |
| if cls._fernet is None: | |
| key = cls._get_encryption_key() | |
| cls._fernet = Fernet(key) | |
| return cls._fernet | |
| def _get_encryption_key(cls) -> bytes: | |
| """ | |
| Get the encryption key from settings/environment. | |
| Returns: | |
| The Fernet key as bytes | |
| Raises: | |
| EncryptionError: If key is not configured or invalid | |
| """ | |
| key = getattr(settings, "ENCRYPTION_KEY", None) | |
| if not key: | |
| key = os.environ.get("ENCRYPTION_KEY", "") | |
| if not key: | |
| raise EncryptionError( | |
| "ENCRYPTION_KEY not configured. " | |
| 'Generate one with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"' | |
| ) | |
| # Convert string key to bytes if necessary | |
| if isinstance(key, str): | |
| key = key.encode("utf-8") | |
| # Validate key format | |
| try: | |
| Fernet(key) | |
| except Exception as e: | |
| raise EncryptionError(f"Invalid ENCRYPTION_KEY format: {e}") | |
| return key | |
| def encrypt(cls, plaintext: str) -> str: | |
| """ | |
| Encrypt a plaintext string. | |
| Args: | |
| plaintext: The string to encrypt | |
| Returns: | |
| Base64-encoded encrypted string | |
| Raises: | |
| EncryptionError: If encryption fails | |
| """ | |
| if not plaintext: | |
| raise EncryptionError("Cannot encrypt empty value") | |
| try: | |
| fernet = cls._get_fernet() | |
| encrypted = fernet.encrypt(plaintext.encode("utf-8")) | |
| return encrypted.decode("utf-8") | |
| except EncryptionError: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Encryption failed: {e}") | |
| raise EncryptionError(f"Encryption failed: {e}") | |
| def decrypt(cls, encrypted_value: str) -> str: | |
| """ | |
| Decrypt an encrypted string. | |
| Args: | |
| encrypted_value: Base64-encoded encrypted string | |
| Returns: | |
| Decrypted plaintext string | |
| Raises: | |
| EncryptionError: If decryption fails (invalid key or corrupted data) | |
| """ | |
| if not encrypted_value: | |
| raise EncryptionError("Cannot decrypt empty value") | |
| try: | |
| fernet = cls._get_fernet() | |
| decrypted = fernet.decrypt(encrypted_value.encode("utf-8")) | |
| return decrypted.decode("utf-8") | |
| except InvalidToken: | |
| logger.error("Decryption failed: Invalid token (wrong key or corrupted data)") | |
| raise EncryptionError( | |
| "Decryption failed: Invalid encryption key or corrupted data" | |
| ) | |
| except EncryptionError: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Decryption failed: {e}") | |
| raise EncryptionError(f"Decryption failed: {e}") | |
| def generate_key(cls) -> str: | |
| """ | |
| Generate a new Fernet encryption key. | |
| Returns: | |
| A new Fernet key as a string | |
| """ | |
| return Fernet.generate_key().decode("utf-8") | |
| def is_configured(cls) -> bool: | |
| """Check if encryption is properly configured.""" | |
| try: | |
| cls._get_encryption_key() | |
| return True | |
| except EncryptionError: | |
| return False | |
| def reset(cls) -> None: | |
| """Reset the cached Fernet instance (useful for testing).""" | |
| cls._fernet = None | |