Encryption at rest with Django-Citadel

A Django web app may work with sensitive data that you’d like to protect at rest. One solution would be to use an encrypted database engine or extension, such as pgcrypto for PostgreSQL. pgcrypto requires certain privileges to install, which you may not have, and provides low level columnar encryption that still requires some integration effort to use with Django. django-citadel is a Django app I’ve been working on to provide encryption-at-rest capabilities controlled exclusively at the application layer, giving the developer control over the encryption and decryption of model fields on an as-needed basis.

Caveat

Encryption is hard, and I’m not a professional cryptographer. django-citadel primarily serves as a platform for me to practice working with AES, PBKDF2 hashing, and modifying the Django ORM. Before you deploy it to protect your bitcoin passwords, please give it a thorough review.

Overview

Django’s ORM allows for models that represent entities in the application (such as a user, blog post, auction item, etc). Models have fields, which define the attributes of the model type. A number of stock field types are available, such as CharField, DateField, and ForeignKey. An example of a simple model is below:

from django.db import Models
from django.contrib.auth.models import User

class BuriedTreasure(models.Model):
    user = models.ForeinKey(User)
    location = models.CharField(max_length=100)

If I create an instance of BuriedTreasure:

bt = BuriedTreasure.objects.create(
    user=me,
    location="Under the tree")

the model is saved to the database through a cleansing and serialization process. In this case, the CharField will be saved in plaintext, which is bad news if Blackbeard makes off with the whole database.

If we use the drop-in django-citadel componentry, we have:

from django.contrib.auth.models import User
from citadel.fields import SecretField
from citadel.models import SecretiveModel

class BuriedTreasure(SecretiveModel):
    user = models.ForeinKey(User)
    location = SecretField()

bt = BuriedTreasure.objects.create(
    user=me,
    location=Secret.from_plaintext(plaintext="Under the tree",
                                   password="VerySecretPassword1234")

The location attribute will then be encrypted via AES prior to serialization. The encryption key is generated by hashing the password with the PBKDF2 algorithm. The result is that the database only stores an encrypted string, a salt, and a hash of the plaintext (for authentication purposes). The plaintext and password are never provided to the database server.

Decrypting the secret is as simple as:

plaintext = bt.location.get_plaintext(password="VerySecretPassword1234")

In cases where the user provides the password at the time of decryption (say, during login), the app server doesn’t even need to store the decryption key, meaning a total compromise of the entire stack would still leave an attacker with no means of decrypting the Secret.

Features