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

2024-06-14 20:16:13 发布

您现在位置:Python中文网/ 问答频道 /正文

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

custom fields documentation说:

  1. 添加__metaclass__ = models.SubfieldBase
  2. 重写到python以将数据从其原始存储转换为所需格式
  3. 重写get_prep_value以在存储到数据库之前转换该值。

所以你觉得这很简单-两个人。只需解密值,然后3。只要加密就行了。

松散地基于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

保存模型时,由于尝试解密已解密的字符串,会发生奇数长度字符串错误。在调试时,python似乎被调用了两次,第一次调用的是加密的值,第二次调用的是解密的值,但实际上不是解密的类型,而是原始字符串,导致了错误。此外,从未调用get_prep_value。

我做错什么了?

这不应该那么难-有没有人认为Django字段代码写得很差,特别是在自定义字段方面,而且没有那么可扩展?简单的可重写的pre-save和post-fetch方法很容易解决这个问题。


Tags: selfreturnifvaluedeftypenotit
3条回答

您应该重写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)

这样可以防止多次解密。

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

  1. 当Django将数据库中的一个值分配给字段时(也就是要解密该值的时候)
  2. 手动为自定义字段赋值时,例如record.field=value

您可以使用的一个技巧是将前缀或后缀添加到值字符串并检查它,而不是执行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?

你忘了设置元类:

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

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

相关问题 更多 >