如何创建加密的django字段,在从数据库中检索数据时转换数据?

时间:2012-10-25 20:54:30

标签: python django pycrypto

我有一个自定义的EncryptedCharField,我希望在连接UI时基本上显示为CharField,但在数据库中存储/检索之前,它会加密/解密它。

custom fields documentation说:

  1. 添加__metaclass__ = models.SubfieldBase
  2. 覆盖to_python以将数据从其原始存储转换为所需格式
  3. 覆盖get_prep_value以在存储db之前转换该值。
  4. 所以你认为这很容易 - 只需要解密这个值,然后再加密它。

    松散地基于a django snippet,以及此字段的文档:

    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
    

    和加密器看起来像:

    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方法可以轻松解决这个问题。

5 个答案:

答案 0 :(得分:8)

我认为问题在于,当您为自定义字段分配值时,也会调用to_python(因为验证的一部分可能基于this link)。所以问题是在以下情况下区分to_python调用:

  1. 当Django将数据库中的值分配给该字段时(当您要解密该值时)
  2. 手动为自定义字段指定值时,例如record.field = value
  3. 您可以使用的一个hack是为值字符串添加前缀或后缀并检查它而不是执行 isinstance 检查。

    我打算写一个例子,但我找到了这个(甚至更好:))。

    检查 BaseEncryptedField https://github.com/django-extensions/django-extensions/blob/master/django_extensions/db/fields/encrypted.py

    来源Django Custom Field: Only run to_python() on values from DB?

答案 1 :(得分:4)

你应该覆盖to_python,就像代码片一样。

如果您查看CharField课程,可以看到它没有value_to_string方法:

docsto_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)

这可以防止您多次解密。

答案 2 :(得分:3)

您忘了设置元类:

class EncryptedCharField(models.CharField):
    __metaclass__ = models.SubfieldBase

custom fields documentation解释了为什么这是必要的。

答案 3 :(得分:1)

您需要添加一个处理多种情况的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(前者用于特定于数据库的转换 - 请参阅https://docs.djangoproject.com/en/1.4/howto/custom-model-fields/#converting-python-objects-to-query-values

答案 4 :(得分:0)

自从最初回答这个问题以来,已经编写了许多软件包来解决这个确切的问题。

例如,自2018年起,软件包django-encrypted-model-fields使用类似的语法

from encrypted_model_fields.fields import EncryptedCharField

class MyModel(models.Model):
    encrypted_char_field = EncryptedCharField(max_length=100)
    ...

根据经验,当存在更成熟的解决方案时,将自己的解决方案推向安全挑战通常是一个坏主意-社区是比您更好的测试人员和维护者。