Как создать зашифрованное поле django, которое преобразует данные при извлечении из базы данных?

У меня есть собственный EncryptedCharField, который я хочу в основном отображать как CharField при взаимодействии с пользовательским интерфейсом, но перед сохранением/извлечением в БД он шифрует/расшифровывает его.

документация по пользовательским полям сообщает:

  • добавить __metaclass__ = models.SubfieldBase
  • переопределить to_python для преобразования данных из него необработанного хранилища в желаемый формат
  • переопределить get_prep_value для преобразования значения перед сохранением db.

Итак, вы думаете, что этого будет достаточно просто - для 2. просто расшифруйте значение и 3. просто зашифруйте его.

Основываясь на фрагменте django, и в документации это поле выглядит следующим образом:

class EncryptedCharField(models.CharField):
  """Just like a char field, but encrypts the value before it enters the database, and    decrypts it when it
  retrieves it"""
  __metaclass__ = models.SubfieldBase
  def __init__(self, *args, **kwargs):
    super(EncryptedCharField, self).__init__(*args, **kwargs)
    cipher_type = kwargs.pop('cipher', 'AES')
    self.encryptor = Encryptor(cipher_type)

  def get_prep_value(self, value):
     return encrypt_if_not_encrypted(value, self.encryptor)

  def to_python(self, value):
    return decrypt_if_not_decrypted(value, self.encryptor)


def encrypt_if_not_encrypted(value, encryptor):
  if isinstance(value, EncryptedString):
    return value
  else:
    encrypted = encryptor.encrypt(value)
    return EncryptedString(encrypted)

def decrypt_if_not_decrypted(value, encryptor):
  if isinstance(value, DecryptedString):
    return value
  else:
    encrypted = encryptor.decrypt(value)
    return DecryptedString(encrypted)


class EncryptedString(str):
  pass

class DecryptedString(str):
  pass

и Encryptor выглядит так:

class Encryptor(object):
  def __init__(self, cipher_type):
    imp = __import__('Crypto.Cipher', globals(), locals(), [cipher_type], -1)
    self.cipher = getattr(imp, cipher_type).new(settings.SECRET_KEY[:32])

  def decrypt(self, value):
    #values should always be encrypted no matter what!
    #raise an error if tthings may have been tampered with
    return self.cipher.decrypt(binascii.a2b_hex(str(value))).split('\0')[0]

  def encrypt(self, value):
    if value is not None and not isinstance(value, EncryptedString):
      padding  = self.cipher.block_size - len(value) % self.cipher.block_size
      if padding and padding < self.cipher.block_size:
        value += "\0" + ''.join([random.choice(string.printable) for index in range(padding-1)])
      value = EncryptedString(binascii.b2a_hex(self.cipher.encrypt(value)))
    return value

При сохранении модели возникает ошибка строки "Нечетная длина" в результате попытки расшифровать уже расшифрованную строку. При отладке он выглядит как to_python, который вызывается дважды, первый с зашифрованным значением, а второй раз с расшифрованным значением, но не на самом деле как тип Decrypted, а как необработанная строка, вызывающая ошибку. Кроме того, get_prep_value никогда не вызывается.

Что я делаю неправильно?

Это не должно быть так сложно: кто-нибудь еще считает, что этот код поля Django очень плохо написан, особенно когда речь заходит о настраиваемых полях, а не о расширяемости? Простые переопределяемые методы pre_save и post_fetch легко решают эту проблему.

Ответ 1

Я думаю, проблема в том, что to_python также вызывается, когда вы назначаете значение своему настраиваемому полю (как часть проверки может быть, на основе эта ссылка). Поэтому проблема заключается в том, чтобы различать вызовы to_python в следующих ситуациях:

  • Когда значение из базы данных назначается в поле Django (это, когда вы хотите расшифровать значение)
  • Когда вы вручную назначаете значение настраиваемому полю, например. record.field = значение

Один взлом, который вы можете использовать, - это добавить префикс или суффикс в строку значений и проверить это вместо выполнения проверки isinstance.

Я собирался написать пример, но нашел этот (еще лучше:)).

Проверить BaseEncryptedField: https://github.com/django-extensions/django-extensions/blob/master/django_extensions/db/fields/encrypted.py

Источник: Django Custom Field: запускать to_python() для значений из DB?

Ответ 2

Вы должны переопределить to_python, как это сделал фрагмент.

Если вы посмотрите на класс CharField, вы увидите, что у него нет метода value_to_string:

docs говорят, что метод to_python должен иметь дело с тремя вещами:

  • Пример правильного типа
  • Строка (например, из десериализатора).
  • Независимо от того, что база данных возвращает для используемого типа столбца.

В настоящее время вы имеете дело только с третьим случаем.

Один из способов справиться с этим - создать специальный класс для расшифрованной строки:

class DecryptedString(str):
   pass

Затем вы можете обнаружить этот класс и обработать его в to_python():

def to_python(self, value):
    if isinstance(value, DecryptedString):
        return value

    decrypted = self.encrypter.decrypt(encrypted)
    return DecryptedString(decrypted)

Это предотвратит дешифрование более одного раза.

Ответ 4

Вам нужно добавить метод to_python, который имеет дело с несколькими случаями, включая передачу уже дешифрованного значения

(предупреждение: фрагмент отключен от моего собственного кода - только для иллюстрации)

def to_python(self, value):
    if not value:
        return
    if isinstance(value, _Param): #THIS IS THE PASSING-ON CASE
        return value
    elif isinstance(value, unicode) and value.startswith('{'):
        param_dict = str2dict(value)
    else:
        try:
            param_dict = pickle.loads(str(value))
        except:
            raise TypeError('unable to process {}'.format(value))
    param_dict['par_type'] = self.par_type
    classname = '{}_{}'.format(self.par_type, param_dict['rule'])
    return getattr(get_module(self.par_type), classname)(**param_dict)

Кстати:

Вместо get_db_prep_value вы должны использовать get_prep_value (первый для конкретных преобразований db - см. https://docs.djangoproject.com/en/1.4/howto/custom-model-fields/#converting-python-objects-to-query-values)