Files
octopus/myproject/integrations/fields.py

127 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Кастомные поля 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