How can I create an encrypted django field that converts data when it's retrieved from the database?

I think the issue is that to_python is also called when you assign a value to your custom field (as part of validation may be, based on this link). So the problem is to distinguish between to_python calls in the following situations:

  1. When a value from the database is assigned to the field by Django (That's when you want to decrypt the value)
  2. When you manually assign a value to the custom field, e.g. record.field = value

One hack you could use is to add prefix or suffix to the value string and check for that instead of doing isinstance check.

I was going to write an example, but I found this one (even better :)).

Check BaseEncryptedField: https://github.com/django-extensions/django-extensions/blob/2.2.9/django_extensions/db/fields/encrypted.py (link to an older version because the field was removed in 3.0.0; see Issue #1359 for reason of deprecation)

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


You should be overriding to_python, like the snippet did.

If you take a look at the CharField class you can see that it doesn't have a value_to_string method:

  • django/db/models/fields/__init__.py

The docs say that the to_python method needs to deal with three things:

  • An instance of the correct type
  • A string (e.g., from a deserializer).
  • Whatever the database returns for the column type you're using.

You are currently only dealing with the third case.

One way to handle this is to create a special class for a decrypted string:

class DecryptedString(str):
   pass

Then you can detect this class and handle it in to_python():

def to_python(self, value):
    if isinstance(value, DecryptedString):
        return value

    decrypted = self.encrypter.decrypt(encrypted)
    return DecryptedString(decrypted)

This prevents you from decrypting more than once.


You forgot to set the metaclass:

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

The custom fields documentation explains why this is necessary.