Archive
Tags
android (3)
ant (2)
beautifulsoup (1)
debian (1)
decorators (1)
django (9)
dovecot (1)
encryption (1)
fix (4)
gotcha (2)
hobo (1)
htmlparser (1)
imaplib (2)
java (1)
json (2)
kerberos (2)
linux (7)
lxml (5)
markdown (4)
mechanize (6)
multiprocessing (1)
mysql (2)
nagios (2)
new_features (3)
open_source (5)
optparse (2)
parsing (1)
perl (2)
postgres (1)
preseed (1)
pxe (4)
pyqt4 (1)
python (41)
raid (1)
rails (1)
red_hat (1)
reportlab (4)
request_tracker (2)
rt (2)
ruby (1)
scala (1)
screen_scraping (7)
shell_scripting (8)
soap (1)
solaris (3)
sql (2)
sqlalchemy (2)
tips_and_tricks (1)
twitter (2)
ubuntu (1)
vmware (2)
windows (1)
zimbra (2)

This last week I was putting together a password management application for our group at work. The biggest obstacle was adding encryption to the password data in the database. At first I thought, "I'll use Django's password field." Django uses one-way hashes like all good authentication systems though. I solved this by making my own custom field, extended from django's CharField.

The first step was learning how to do cryptography in python. I found PyCrypto. I does both ciphers and hashes in various algorithms. I chose AES for my algorithm.

AES has a few requirements. The key must be 16, 24, or 32 characters and the length of the string to be encrypted must be a multiple of 16. The key isn't bad, but the string length is a pain. I made a little wrapper to simplify this, so a string of any length my be passed.

 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
#!/usr/bin/env python
import string
from random import choice
from Crypto.Cipher import AES

EOD = '`%EofD%`' # This should be something that will not occur in strings

def genstring(length=16, chars=string.printable):
    return ''.join([choice(chars) for i in range(length)])

def encrypt(key, s):
    obj = AES.new(key)
    datalength = len(s) + len(EOD)
    if datalength < 16:
        saltlength = 16 - datalength
    else:
        saltlength = 16 - datalength % 16
    ss = ''.join([s, EOD, genstring(saltlength)])
    return obj.encrypt(ss)

def decrypt(key, s):
    obj = AES.new(key)
    ss = obj.decrypt(s)
    return ss.split(EOD)[0]

if __name__ == '__main__':
    for i in xrange(8, 20):
        s = genstring(i)
        key = genstring(32)
        print 'The key is', key
        print 'The string is', s, i
        cipher  = encrypt(key, s)
        print 'The encrypted string is', cipher
        print 'This decrypted string is', decrypt(key, cipher)

If you execute this, it will generate a key and a string, plus encrypt and decrypt the string. This gives binary data. Binary data and databases don't mix though. Django 1.0 turns everything into unicode, which further complicates things. The trick is to translate the binary into something Django and databases can use easily, like base64. We do this with my new favorite module, binascii. Here's an example of its use:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python
import binascii
import qaes # This is the wrapper for PyCrypto

key = "32 character key can be anything"

s = "Sensitive Data"
print "Unencrypted data:", s

es = qaes.encrypt(key, s)
print "Encrypted binary:", es

esb64 = binascii.b2a_base64(es)
print "Encrypted base64:", esb64

esbin = binascii.a2b_base64(esb64)
print "Back to binary:", esbin

ds = qaes.decrypt(key, esbin)
print "Decrypted data", ds

If you run this, you'll see a line like, Encrypted base64: vF7nMm7N1EhalQ/4gtQhaxEHeCgY2dOsNf2rA7tQZW8=. Everytime you run it though, you'll get a different string because of the random salt on the end.

All that's left now is our custom field. I based this on CharField, but you could use a TextField here too.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import binascii
import qaes
from django.db import models
from django.conf import settings

class AESEncryptedField(models.CharField):
    def save_form_data(self, instance, data):
        setattr(instance, self.name,
            binascii.b2a_base64(qaes.encrypt(settings.AESKEY, data)))
    def value_from_object(self, obj):
        return qaes.decrypt(settings.AESKEY,
            binascii.a2b_base64(getattr(obj, self.attname)))

This requires the user to have a AESKEY set in the settings.py of their project. After that, it is transparent and works like any other CharField!

Posted by Tyler Lesmann on December 19, 2008 at 11:47 and commented on 3 times
Tagged as: django encryption python