Encryption at rest with Django-Citadel
Fri, Jan 16, 2015A 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
- Each secret receives a unique salt value and initialization vector.
- Password hashing is performed using PBKDF2 hash function in order to combat rainbow attacks.
- PBKDF2’s work factor is tied to the underlying Django version, eliminating the need for separate maintenance. Django upgrades trigger an automatic re-encryption whenever a secret is decrypted.
- Hydrated fields remain encrypted in memory until explicitly decrypted, limiting unnecessary performance overhead and plaintext leakage.
- Each SecretField can be decrypted individually, providing granular control.
- Each secret can be encrypted with a unique key, ideal for encryption using user-provided keys.