User Tools

Site Tools


python:custom_encrypted_field

Custom Encrypted Model Field

17.08.2014

Pre requisites

This code was written for Django 1.6 and Python 2.7.
It's using AES as encryption in CBC mode.

The goal

We need to create a custom Model field to be used to encrypt different information (as passwords, sensitive information, etc). We mention that we don't need only a hashing function, but more like two ways encryption: we need to be capable of encrypting, but in the same time decrypting information we put into the database.

The encryption should deal with UTF-8; it builds the encrypted string as:

<prefix><IV_used_to_encrypt><encrypted_data>

The default prefix is '_', but you can specify it in the init method. This prefix is used to make a difference between encrypted and non-encrypted data, so make sure you choose an unique one (related to your encryption method).

The code

This code could go in a custom fields.py in your project.

The important mention here is that you store your secret key in settings.py, under the name FIELD_ENCRYPTION_KEY.

# -*- coding: utf-8 -*-
 
import types
import base64
 
from Crypto import Random
from Crypto.Cipher import AES
 
from django.db import models
from yourapp.settings import FIELD_ENCRYPTION_KEY
 
 
class EncryptedField(models.Field):
    __metaclass__ = models.SubfieldBase
 
    def __init__(self, *args, **kwargs):
        self.prefix = kwargs.pop('prefix', '_')
        super(EncryptedField, self).__init__(*args, **kwargs)
 
    def get_internal_type(self):
        return 'TextField'
 
    def to_python(self, value):
        if value is None or not isinstance(value, types.StringTypes):
            return value
 
        if self.is_encrypted(value):
            value = value[len(self.prefix):]    # cut prefix
            value = base64.b64decode(value)
 
            iv, encrypted = value[:AES.block_size], value[AES.block_size:]  # extract iv
 
            crypto = AES.new(FIELD_ENCRYPTION_KEY[:32], AES.MODE_CBC, iv)
            raw_decrypted = crypto.decrypt(encrypted)
            value = raw_decrypted.rstrip("\0").decode('unicode_escape')
 
        return value
 
    def get_db_prep_value(self, value, connection, prepared=False):
        if not prepared:
            iv = Random.new().read(AES.block_size)
            crypto = AES.new(FIELD_ENCRYPTION_KEY[:32], AES.MODE_CBC, iv)
 
            if isinstance(value, types.StringTypes):
                value = value.encode('unicode_escape')
                value = value.encode('ascii')
            else:
                value = str(value)
 
            tag_value = (value + (AES.block_size - len(value) % AES.block_size) * "\0")
            value = self.prefix + base64.b64encode(iv + crypto.encrypt(tag_value))
 
        return value
 
    def is_encrypted(self, value):
        """checks if a string is encrypted against a static predefined prefix"""
        if self.prefix and value.startswith(self.prefix):
            return True
        else:
            return False

How it's used

In your model, assuming that you have a module utils/models

from utils.models.fields import EncryptedField
...
class Example(models.Model):
    name = models.CharField(max_length=128, db_index=True, unique=True)
    password = EncryptedField()

python/custom_encrypted_field.txt · Last modified: 2014/08/17 10:16 by admin