Login

Encryption Fields

Author:
zbyte64
Posted:
September 29, 2008
Language:
Python
Version:
1.0
Score:
5 (after 5 ratings)

This provides some basic cryptographic fields using pyCrypto. All encryption/decription is done transparently and defaults to use AES. Example usage:

class DefferredJunk(models.Model):
    semi_secret = EncryptedCharField(max_length=255)
 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
from django.db import models
from django import forms
from django.conf import settings
import binascii
import random
import string

class EncryptedString(str):
    """A subclass of string so it can be told whether a string is
       encrypted or not (if the object is an instance of this class
       then it must [well, should] be encrypted)."""
    pass

class BaseEncryptedField(models.Field):
    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])
        models.Field.__init__(self, *args, **kwargs)
        
    def to_python(self, value):
        #values should always be encrypted no matter what!
        #raise an error if tthings may have been tampered with
        return self.cipher.decrypt(binascii.a2b_hex(str(value))).split('\0')[0]
    
    def get_db_prep_value(self, value):
        if value is not None and not isinstance(value, EncryptedString):
            padding  = self.cipher.block_size - len(value) % self.cipher.block_size
            if padding and padding < self.cipher.block_size:
                value += "\0" + ''.join([random.choice(string.printable) for index in range(padding-1)])
            value = EncryptedString(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)

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 2 months ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 2 months, 1 week ago
  3. Serializer factory with Django Rest Framework by julio 9 months, 1 week ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 9 months, 4 weeks ago
  5. Help text hyperlinks by sa2812 10 months, 3 weeks ago

Comments

svetlyak (on October 1, 2008):

I seems, that you need to make real max_length twice bigger, than len(value)+padding, because encrypted data is bigger.

#

jonknee (on October 1, 2008):

Using a credit card number as an example for this is a bad idea. This isn't PCI compliant and storing CC numbers on your server is a great way to get into a lot of trouble (not to mention pissing your customers off). There is almost no reason to store a CC number, gateways like Authorize.net can do that for you and stay completely compliant.

Also no credit card company in the world allows you to keep CVV for any amount of time, so that shouldn't ever be in your model. Even the gateways can't keep this information.

#

zbyte64 (on October 8, 2008):

Good point jonknee, modified the example as such

#

noop (on April 28, 2009):

I am really new to this, but when i try to implement this i keep getting a TypeError: "Non-hexadecimal digit found"

my code looks like this:

newObj = Obj.objects.create(name=username)

newObj.password = passw

newObj.save()

What am i doing wrong? Thanks for the help

#

Helljawz (on April 10, 2011):

How could it be adapted for FileField's?

#

therealzam (on April 17, 2012):

To be compatible with Django 1.4, the following updates are needed for line 26: OLD = def get_db_prep_value(self, value): NEW = def get_db_prep_value(self, value, connection, prepared=False):

#

ffsffd (on April 2, 2013):

Actually it's enough specify the encryption mode to either cipher.MODE_CBC or cipher.MODE_CTR, no need to delete the snippet :)

I guess the easiest would be CTR (stream cipher mode), which allows the plaintext to be of any length, so you won't need to mess with padding any more; an example using your code would be

def __init__(self, *args, **kwargs)
    ...
    imp = __import__('Crypto.Cipher', [], [], [cipher], -1)
    from Crypto.Util import Counter
    self.cipher = getattr(imp, cipher)
    self.counter = Counter
    ...

def encrypt(self, text):
    counter = self.counter.new(128)
    cipher = self.cipher.new(settings.SECRET_KEY[:32], mode=self.cipher.MODE_CTR, counter=counter)
    return cipher.encrypt(text)

def decrypt(self, ciphertext):
    counter = self.counter.new(128)
    cipher = self.cipher.new(settings.SECRET_KEY[:32], mode=self.cipher.MODE_CTR, counter=counter)
    return cipher.decrypt(ciphertext)

the reason for using separate instances for encryption and decryption is the fact that you can't encrypt and decrypt with the same cipher instance (the counter will yield a different key), so you need your counter at the same point when decrypting

#

drekorig (on June 7, 2013):

To avoid TypeError: "Non-hexadecimal digit found".

def to_python(self, value):
    #values should always be encrypted no matter what!
    #raise an error if tthings may have been tampered with
    try:
      binascii.a2b_hex(str(value))
    except:
      value = self.get_db_prep_value(value, connection)
    return self.cipher.decrypt(binascii.a2b_hex(str(value))).split('\0')[0]

Also, You need import "connection" as : from django.db import connection

And change the "get_db_prep_value" method as :

def get_db_prep_value(self, value, connection=connection, prepared=False):
    if value is not None and not isinstance(value, EncryptedString):
        padding  = self.cipher.block_size - len(value) % self.cipher.block_size
        if padding and padding < self.cipher.block_size:
            value += "\0" + ''.join([random.choice(string.printable) for index in range(padding-1)])
        value = EncryptedString(binascii.b2a_hex(self.cipher.encrypt(value)))
    return value

#

Please login first before commenting.