Django Encryption – An Updated How-To
I love Django, and I love Django Snippets, but I’ve noticed some snippets are out of date, most notably for me, Django snippet 1095 or Django Encryption. Unfortunately, some folks are hitting a few snags on TypeError: “Non-hexadecimal digit found”.
Luckily, it seems that Django-Fields have solved this problem for us! Here is my (their) technique!
Make a file named encryption.py to go into the same folder as your settings.py containing:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | import binascii import random import string from django import forms from django.db import models from django.conf import settings class BaseEncryptedField(models.Field): '''This code is based on the djangosnippet #1095 You can find the original at http://www.djangosnippets.org/snippets/1095/''' def __init__(self, *args, **kwargs): cipher = kwargs.pop('cipher', 'AES') imp = __import__('Crypto.Cipher', globals(), locals(), [cipher], -1) self.cipher = getattr(imp, cipher).new(settings.SECRET_KEY[:32]) self.prefix = '$%s$' % cipher max_length = kwargs.get('max_length', 40) mod = max_length % self.cipher.block_size if mod > 0: max_length += self.cipher.block_size - mod kwargs['max_length'] = max_length * 2 + len(self.prefix) models.Field.__init__(self, *args, **kwargs) def _is_encrypted(self, value): return isinstance(value, basestring) and value.startswith(self.prefix) def _get_padding(self, value): mod = len(value) % self.cipher.block_size if mod > 0: return self.cipher.block_size - mod return 0 def to_python(self, value): if self._is_encrypted(value): return self.cipher.decrypt(binascii.a2b_hex(value[len(self.prefix):])).split('\0')[0] return value def get_db_prep_value(self, value): if value is not None and not self._is_encrypted(value): padding = self._get_padding(value) if padding > 0: value += "\0" + ''.join([random.choice(string.printable) for index in range(padding-1)]) value = self.prefix + binascii.b2a_hex(self.cipher.encrypt(value)) return value class EncryptedTextField(BaseEncryptedField): __metaclass__ = models.SubfieldBase def get_internal_type(self): return 'TextField' def formfield(self, **kwargs): defaults = {'widget': forms.Textarea} defaults.update(kwargs) return super(EncryptedTextField, self).formfield(**defaults) class EncryptedCharField(BaseEncryptedField): __metaclass__ = models.SubfieldBase def get_internal_type(self): return "CharField" def formfield(self, **kwargs): defaults = {'max_length': self.max_length} defaults.update(kwargs) return super(EncryptedCharField, self).formfield(**defaults)) |
And then in your models.py:
1 2 3 4 5 6 7 8 9 10 | from encryption import EncryptedCharField ... class Example(models.Model): secret = EncryptedCharField(max_length=255) class Meta: ordering = ('secret',) def __unicode__(self): return self.secret |
This should be pretty explanatory! Have fun!
PS: You need PyCrypto! Google much?
Bryan is a jazz and blues guitarist, small-time designer, Python hacker, entrepreneur, and lover of fine whiskeys. He's the man behind such sites as
Thanks Bryan, however django always ask to renter this filed when saving my admin form. can’t it ignore the field when no new data is typed in?
my bad I was using PasswordInput widget with render_value=False
I tried adapting it for FileField’s but it didn’t work. How would you do it?
Thanks.
Hi,
I’m trying hard to implement your snipped but using initialization vector. I have no problem on declaring iv into __init__, but the problem seems to come when decrypting: I use a randomized iv, so each db entry has its own iv, and I include it after the prefix. I think that I’m doing it well when extracting iv from the data (value), but it fails to decrypt. I think that it can be caused by the fact that cipher is declared in __init__, and in your example is basically the same every time it’s called. So, how can I fix it?
Thanks, very helpful.
I just added multi laguage support
def to_python(self, value):
if self._is_encrypted(value):
return self.cipher.decrypt(binascii.a2b_hex(value[len(self.prefix):])).split(”)[0].decode(‘utf8′)
return value
def get_db_prep_value(self, value, connection, prepared=False):
if value is not None and not self._is_encrypted(value):
value = value.encode(‘utf8′)
padding = self._get_padding(value)
if padding > 0:
value += “” + ”.join([random.choice(string.printable) for index in range(padding - 1)])
value = self.prefix + binascii.b2a_hex(self.cipher.encrypt(value))
return value
Thanks for the script, just a few issues that I ran into
If you get an error on line 70:
return super(EncryptedCharField, self).formfield(**defaults))
remove the last ‘)’.
I know it seems obvious, but I went off and learnt about the super object to fix the problem, only to discover that I should have just done simple syntax checking first.
Also as Shay showed in his addition, change:
def get_db_prep_value(self, value):
to
def get_db_prep_value(self, value, connection, prepared=False):
For future proofing.
And if you are using South, add:
from south.modelsinspector import add_introspection_rules
add_introspection_rules([], ["^encryption\.EncryptedCharField"])
Again, thanks for the great script. My comments were only help out people who hit the same problems I did, using a script from 2009.