Bryan Helmig

Co-founder of Zapier, speaker, musician and builder of things.

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?


Posted October 16, 2009 @ 12:56 am under Boring Stuff.