December 2009
November 2009
October 2009
September 2009
June 2009
April 2009
March 2009
February 2009
January 2009
December 2008
November 2008
October 2008
July 2008
June 2008
October 2007
September 2007
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!
