我有一个自定义的EncryptedCharField,我希望在连接UI时基本上显示为CharField,但在数据库中存储/检索之前,它会加密/解密它。
__metaclass__ = models.SubfieldBase
所以你认为这很容易 - 只需要解密这个值,然后再加密它。
松散地基于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方法可以轻松解决这个问题。
答案 0 :(得分:8)
我认为问题在于,当您为自定义字段分配值时,也会调用to_python(因为验证的一部分可能基于this link)。所以问题是在以下情况下区分to_python调用:
您可以使用的一个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
方法:
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)
这可以防止您多次解密。
答案 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)
...
根据经验,当存在更成熟的解决方案时,将自己的解决方案推向安全挑战通常是一个坏主意-社区是比您更好的测试人员和维护者。