我有一个自定义的 EncryptedCharField,我希望在连接 UI 时基本上显示为 CharField,但在数据库中存储/检索之前,它会对其进行加密/解密。
自定义字段文档说:
- 添加
__metaclass__ = models.SubfieldBase
- 覆盖 to_python 以将数据从其原始存储转换为所需的格式
- 覆盖 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 方法可以轻松解决这个问题。