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

11👍

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?

4👍

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:

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.

3👍

You forgot to set the metaclass:

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

The custom fields documentation explains why this is necessary.

2👍

Since this question was originally answered, a number of packages have been written to solve this exact problem.

For example, as of 2018, the package django-encrypted-model-fields handles this with a syntax like

from encrypted_model_fields.fields import EncryptedCharField

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

As a rule of thumb, it’s usually a bad idea to roll your own solution to a security challenge when a more mature solution exists out there — the community is a better tester and maintainer than you are.

1👍

You need to add a to_python method that deals with a number of cases, including passing on an already decrypted value

(warning: snippet is cut from my own code – just for illustration)

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)

By the way:

Instead of get_db_prep_value you should use get_prep_value (the former is for db specific conversions – see https://docs.djangoproject.com/en/1.4/howto/custom-model-fields/#converting-python-objects-to-query-values )

Leave a comment