18

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

定义字段文档说:

  1. 添加__metaclass__ = models.SubfieldBase
  2. 覆盖 to_python 以将数据从其原始存储转换为所需的格式
  3. 覆盖 get_prep_value 以在存储数据库之前转换值。

所以你认为这很容易 - 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

加密器看起来像:

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

4

5 回答 5

11

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

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

您可以使用的一种技巧是向值字符串添加前缀或后缀并进行检查,而不是进行isinstance检查。

我打算写一个例子,但我发现了这个(更好:))。

检查BaseEncryptedFieldhttps://github.com/django-extensions/django-extensions/blob/2.2.9/django_extensions/db/fields/encrypted.py(链接到旧版本,因为该字段在 3.0.0 中被删除;有关弃用的原因,请参见问题 #1359 )

来源Django 自定义字段:仅对 DB 中的值运行 to_python()?

于 2012-11-12T14:20:22.007 回答
4

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

如果您查看CharField该类,您会发现它没有value_to_string方法:

文档说该方法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)

这可以防止您多次解密。

于 2012-10-25T20:56:49.887 回答
3

您忘记设置元类:

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

定义字段文档解释了为什么这是必要的。

于 2012-10-25T21:44:54.897 回答
2

由于最初回答了这个问题,因此已经编写了许多包来解决这个确切的问题。

例如,截至 2018 年,包django-encrypted-model-fields使用如下语法处理此问题

from encrypted_model_fields.fields import EncryptedCharField

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

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

于 2018-09-27T21:50:26.560 回答
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

于 2012-10-25T21:50:14.160 回答