""" Кастомные поля Django с шифрованием для безопасного хранения credentials. Использует Fernet (AES-128-CBC) из библиотеки cryptography. """ from django.db import models from django.conf import settings from cryptography.fernet import Fernet, InvalidToken class EncryptedCharField(models.CharField): """ CharField с прозрачным шифрованием/дешифрованием. Данные шифруются при сохранении в БД и дешифруются при чтении. В БД хранится зашифрованная строка (base64). Требует ENCRYPTION_KEY в settings.py: from cryptography.fernet import Fernet ENCRYPTION_KEY = Fernet.generate_key() # сгенерировать один раз! Пример использования: api_token = EncryptedCharField(max_length=500, blank=True) """ description = "Encrypted CharField using Fernet" def __init__(self, *args, **kwargs): # Сохраняем оригинальный max_length для deconstruct() self._original_max_length = kwargs.get('max_length') # Зашифрованные данные длиннее исходных, увеличиваем max_length if 'max_length' in kwargs: # Fernet добавляет ~100 байт overhead kwargs['max_length'] = max(kwargs['max_length'] * 2, 500) super().__init__(*args, **kwargs) def deconstruct(self): """Возвращаем оригинальные параметры для миграций""" name, path, args, kwargs = super().deconstruct() # Восстанавливаем оригинальный max_length if self._original_max_length is not None: kwargs['max_length'] = self._original_max_length return name, path, args, kwargs def _get_fernet(self): """Получить инстанс Fernet с ключом из settings""" key = getattr(settings, 'ENCRYPTION_KEY', None) if not key: raise ValueError( "ENCRYPTION_KEY не найден в settings. " "Сгенерируйте ключ: from cryptography.fernet import Fernet; Fernet.generate_key()" ) # Ключ может быть строкой или bytes if isinstance(key, str): key = key.encode() return Fernet(key) def get_prep_value(self, value): """Шифрование перед сохранением в БД""" value = super().get_prep_value(value) if value is None or value == '': return value try: f = self._get_fernet() encrypted = f.encrypt(value.encode('utf-8')) return encrypted.decode('utf-8') except Exception as e: raise ValueError(f"Ошибка шифрования: {e}") def from_db_value(self, value, expression, connection): """Дешифрование при чтении из БД""" if value is None or value == '': return value try: f = self._get_fernet() decrypted = f.decrypt(value.encode('utf-8')) return decrypted.decode('utf-8') except InvalidToken: # Данные не зашифрованы или ключ изменился # Возвращаем как есть (для миграции старых данных) return value except Exception: return value def to_python(self, value): """Преобразование в Python-объект (не дешифруем, т.к. это для форм)""" return super().to_python(value) class EncryptedTextField(models.TextField): """ TextField с шифрованием для больших данных (например JSON credentials). """ description = "Encrypted TextField using Fernet" def _get_fernet(self): key = getattr(settings, 'ENCRYPTION_KEY', None) if not key: raise ValueError("ENCRYPTION_KEY не найден в settings.") if isinstance(key, str): key = key.encode() return Fernet(key) def get_prep_value(self, value): value = super().get_prep_value(value) if value is None or value == '': return value try: f = self._get_fernet() encrypted = f.encrypt(value.encode('utf-8')) return encrypted.decode('utf-8') except Exception as e: raise ValueError(f"Ошибка шифрования: {e}") def from_db_value(self, value, expression, connection): if value is None or value == '': return value try: f = self._get_fernet() decrypted = f.decrypt(value.encode('utf-8')) return decrypted.decode('utf-8') except InvalidToken: return value except Exception: return value