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:
- When a value from the database is assigned to the field by Django (Thatβs when you want to decrypt the value)
- 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.
- Using Django's built in web server in a production environment
- Django & TastyPie: request.POST is empty
- Performance, load and stress testing in Django
- Django Channels VS Django 3.0 / 3.1?
- Why does Django South 1.0 use iteritems()?
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 )