Skip to main content

Field-level encryption for Django using AES-256-GCM

Project description

django-field-encryption

PyPI PyPI - Python Version

Field-level and file encryption for Django using AES-256-GCM with automatic key rotation support.

Features

  • Field-level encryption: Encrypt sensitive data in Django model fields (CharField, TextField, JSONField, IntegerField, DateField, DateTimeField, EmailField)
  • File encryption: Encrypt uploaded files with dedicated storage backend
  • Blind index fields: Searchable encrypted fields via HMAC-SHA256 hashes
  • Automatic key rotation: Seamlessly rotate encryption keys without data migration
  • Key derivation: Separate keys for fields, files, and hashes using HKDF
  • Tamper detection: AES-GCM provides built-in authentication
  • Multiple key support: Manage multiple encryption key versions
  • Django admin integration: Mask encrypted fields and search via blind indexes

Installation

pip install django-field-encryption

Quick Start

  1. Add to your Django settings:
DATA_PROTECTION_KEYS = {
    'v1': 'your-base64-encoded-32-byte-key',
}
DATA_PROTECTION_ACTIVE_KEY_ID = 'v1'
  1. Use the encrypted fields:
from django.db import models
from django_field_encryption import EncryptedCharField, EncryptedTextField

class MyModel(models.Model):
    secret_name = EncryptedCharField(max_length=100)
    secret_notes = EncryptedTextField()

Configuration

Generate a Master Key

from django_field_encryption import generate_master_key

key = generate_master_key()  # Returns base64-encoded 32-byte key

Multiple Keys and Key Rotation

DATA_PROTECTION_KEYS = {
    'v1': 'old-key-base64...',
    'v2': 'new-key-base64...',
}
DATA_PROTECTION_ACTIVE_KEY_ID = 'v2'

When rotating to a new key, existing encrypted data can be re-encrypted:

from django_field_encryption import FieldEncryptor

# Re-encrypt with the active key
rotated_value = FieldEncryptor.rotate_value(old_encrypted_value)

Field Types

All encrypted fields store data as TextField in the database. Lookups (except isnull) are blocked by default -- use BlindIndexField for searchable encrypted fields.

EncryptedCharField

Encrypted character field stored as TextField:

from django_field_encryption import EncryptedCharField

class UserProfile(models.Model):
    ssn = EncryptedCharField(max_length=20)
    credit_card = EncryptedCharField(max_length=16)

EncryptedTextField

Encrypted text field for longer content:

from django_field_encryption import EncryptedTextField

class Document(models.Model):
    content = EncryptedTextField()
    private_notes = EncryptedTextField()

EncryptedJSONField

Encrypted JSON field with automatic serialization:

from django_field_encryption import EncryptedJSONField

class Settings(models.Model):
    preferences = EncryptedJSONField()
# Usage
obj = Settings.objects.create(
    preferences={'theme': 'dark', 'notifications': True}
)
# Automatically serialized, encrypted, and stored

EncryptedIntegerField

Encrypted integer field:

from django_field_encryption import EncryptedIntegerField

class Account(models.Model):
    balance = EncryptedIntegerField()

EncryptedDateField / EncryptedDateTimeField

Encrypted date and datetime fields:

from django_field_encryption import EncryptedDateField, EncryptedDateTimeField

class Event(models.Model):
    event_date = EncryptedDateField()
    created_at = EncryptedDateTimeField()

EncryptedEmailField

Encrypted email field:

from django_field_encryption import EncryptedEmailField

class Contact(models.Model):
    email = EncryptedEmailField(max_length=255)

EncryptedFieldMixin

Base mixin for creating custom encrypted fields:

from django.db import models
from django_field_encryption import EncryptedFieldMixin

class EncryptedURLField(EncryptedFieldMixin, models.URLField):
    pass

The mixin accepts a strict parameter (default True). When strict=False, decryption failures return the raw value instead of raising an exception.

Blind Index Fields

Enable unique lookups on encrypted fields without decrypting:

from django_field_encryption import EncryptedCharField, BlindIndexField

class UserProfile(models.Model):
    email = EncryptedCharField(max_length=255)
    email_hash = BlindIndexField('email', unique=True, db_index=True)

The hash is auto-computed on save via a pre_save signal. Lookup by hash:

from django_field_encryption import compute_hash

user = UserProfile.objects.get(email_hash=compute_hash('user@example.com'))

Note: bulk_create and bulk_update do not fire pre_save signals -- hashes must be computed manually for bulk operations.

File Encryption

EncryptedFileStorage

Use the encrypted file storage for sensitive file uploads:

from django.db import models
from django_field_encryption import EncryptedFileStorage

class Document(models.Model):
    file = models.FileField(storage=EncryptedFileStorage())

Or use the pre-configured instance:

from django_field_encryption import encrypted_file_storage

class Document(models.Model):
    file = models.FileField(storage=encrypted_file_storage)

BaseEncryptedStorage

Wrap any Django Storage backend with transparent encryption:

from django_field_encryption import BaseEncryptedStorage
from storages.backends.s3boto3 import S3Boto3Storage

class EncryptedS3Storage(BaseEncryptedStorage):
    def __init__(self, **kwargs):
        super().__init__(underlying_storage=S3Boto3Storage(), **kwargs)

Migrations

Adding encryption to a field

Adding encryption requires three migrations: add the encrypted column, backfill with encrypted data, then remove the old column.

1. Add the encrypted field alongside the plaintext field:

class UserProfile(models.Model):
    ssn = models.CharField(max_length=20)
    ssn_encrypted = EncryptedCharField(max_length=20, null=True, blank=True)
python manage.py makemigrations myapp && python manage.py migrate myapp

2. Backfill encrypted data:

python manage.py makemigrations --empty myapp --name encrypt_ssn

Edit the migration:

from django.db import migrations
from django_field_encryption import FieldEncryptor


def encrypt_ssn(apps, schema_editor):
    UserProfile = apps.get_model('myapp', 'UserProfile')
    for obj in UserProfile.objects.iterator():
        if obj.ssn:
            obj.ssn_encrypted = FieldEncryptor.encrypt(obj.ssn)
            obj.save(update_fields=['ssn_encrypted'])


def reverse_encrypt_ssn(apps, schema_editor):
    UserProfile = apps.get_model('myapp', 'UserProfile')
    for obj in UserProfile.objects.iterator():
        if obj.ssn_encrypted:
            obj.ssn = FieldEncryptor.decrypt(obj.ssn_encrypted)
            obj.save(update_fields=['ssn'])


class Migration(migrations.Migration):
    dependencies = [('myapp', '0002_userprofile_ssn_encrypted')]
    operations = [migrations.RunPython(encrypt_ssn, reverse_encrypt_ssn)]
python manage.py migrate myapp

3. Remove the old plaintext field:

class UserProfile(models.Model):
    ssn = EncryptedCharField(max_length=20)
python manage.py makemigrations myapp && python manage.py migrate myapp

Removing encryption from a field

Reverse the three-migration pattern: add a plaintext column, decrypt into it, then remove the encrypted column.

def decrypt_ssn(apps, schema_editor):
    UserProfile = apps.get_model('myapp', 'UserProfile')
    for obj in UserProfile.objects.iterator():
        if obj.ssn:
            obj.ssn_plaintext = FieldEncryptor.decrypt(obj.ssn)
            obj.save(update_fields=['ssn_plaintext'])

Switching encrypted field types

All encrypted fields store as TextField, so no data migration is needed -- just change the model definition and run makemigrations:

# Before
secret = EncryptedCharField(max_length=200)
# After
secret = EncryptedTextField()

Key rotation

python manage.py rotate_encryption_keys --dry-run
python manage.py rotate_encryption_keys
python manage.py rotate_encryption_keys --app-label myapp --batch-size 500

For selective rotation:

from django_field_encryption import FieldEncryptor

for obj in MyModel.objects.all():
    new_value = FieldEncryptor.rotate_value(obj.secret)
    if new_value is not None:
        obj.secret = new_value
        obj.save(update_fields=['secret'])

After rotation, recompute blind index hashes:

from django_field_encryption import compute_hash

for obj in MyModel.objects.all():
    obj.email_hash = compute_hash(obj.email)
    obj.save(update_fields=['email_hash'])

Django Admin

Use the provided mixins to integrate encrypted fields with the Django admin:

from django.contrib import admin
from django_field_encryption import EncryptedFieldAdminMixin, EncryptedSearchMixin
from .models import UserProfile


@admin.register(UserProfile)
class UserProfileAdmin(EncryptedSearchMixin, EncryptedFieldAdminMixin, admin.ModelAdmin):
    list_display = ('name', 'display_ssn', 'created_at')
    search_fields = ('name', 'ssn_hash')
    encrypted_search_fields = {'ssn': 'ssn_hash'}

    encrypted_field_mask = '***encrypted***'
    show_encrypted_in_readonly = True
    exclude_encrypted_from_search = True
  • EncryptedFieldAdminMixin -- masks encrypted fields in list_display, moves them to readonly_fields, and excludes them from search_fields.
  • EncryptedSearchMixin -- enables searching by blind index hash via encrypted_search_fields = {'field_name': 'hash_field_name'}. If the hash field name is None, it defaults to '{field_name}_hash'.

Both mixins work with any ModelAdmin that inherits them.

API Reference

FieldEncryptor

Low-level encryption/decryption operations:

from django_field_encryption import FieldEncryptor

# Encrypt
encrypted = FieldEncryptor.encrypt('sensitive data')
# Returns: 'v1:base64encoded...'

# Decrypt
decrypted = FieldEncryptor.decrypt(encrypted)
# Returns: 'sensitive data'

# Check if value is encrypted with a known key
can_decrypt = FieldEncryptor.can_decrypt(encrypted)  # True/False

# Rotate to active key
rotated = FieldEncryptor.rotate_value(encrypted)
# Returns re-encrypted value or None if already using active key

# Clear key cache (call after changing settings in tests)
FieldEncryptor.clear_cache()

FileEncryptor

File-level encryption:

from django_field_encryption import FileEncryptor

# Encrypt bytes
encrypted, key_id = FileEncryptor.encrypt(b'sensitive file content')
# Returns: (b'ENC2...', 'v1')

# Decrypt
decrypted = FileEncryptor.decrypt(encrypted)
# Returns: b'sensitive file content'

# Check if data is encrypted
is_enc = FileEncryptor.is_encrypted(encrypted)  # True/False

compute_hash

Compute a deterministic HMAC-SHA256 hash for blind indexing:

from django_field_encryption import compute_hash

hash_value = compute_hash('user@example.com')
# Returns: 64-character hex string

generate_master_key

Generate a cryptographically secure master key:

from django_field_encryption import generate_master_key

key = generate_master_key()
# Returns: base64-encoded 32-byte key

Configuration Functions

from django_field_encryption import (
    get_keys_config,
    get_active_key_id,
    get_master_key,
)

keys = get_keys_config()          # Returns dict of key_id -> key
active_key = get_active_key_id()  # Returns currently active key_id
master_key = get_master_key('v1') # Returns raw 32-byte key for key_id

Exceptions

from django_field_encryption import (
    EncryptionError,
    ConfigurationError,
    InvalidKeyError,
    DecryptionError,
    EncryptionNotConfiguredError,
)

All exceptions inherit from EncryptionError. ConfigurationError, InvalidKeyError, and DecryptionError accept optional key_id and other contextual attributes.

Compatibility

  • Django 4.2 LTS is fully supported
  • Django 5.0+ supported
  • Django 6.x supported (constraint is <7.0)

Security Notes

  • Keys are 32 bytes (256 bits) for AES-256
  • Uses AES-GCM (Galois/Counter Mode) for authenticated encryption
  • Each encryption generates a unique 12-byte random nonce
  • Field, file, and hash keys are derived separately using HKDF
  • The library does not encrypt at rest -- data is encrypted/decrypted in memory only
  • High-volume deployments: Rotate keys before reaching ~2^32 encryptions per key to avoid nonce collision risk. See docs/security.md for details.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_field_encryption-0.2.1.tar.gz (22.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_field_encryption-0.2.1-py3-none-any.whl (17.9 kB view details)

Uploaded Python 3

File details

Details for the file django_field_encryption-0.2.1.tar.gz.

File metadata

  • Download URL: django_field_encryption-0.2.1.tar.gz
  • Upload date:
  • Size: 22.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.11

File hashes

Hashes for django_field_encryption-0.2.1.tar.gz
Algorithm Hash digest
SHA256 d90595fd61343d37c694061b706b8d6c5d9857180950fa8c4e55ebf34bf225af
MD5 bb97993c9979aeb36e962338c9336908
BLAKE2b-256 64a61ad49d11562d35872655aeb3031ed796ca95cb71a9bd8881f51d95677af4

See more details on using hashes here.

File details

Details for the file django_field_encryption-0.2.1-py3-none-any.whl.

File metadata

File hashes

Hashes for django_field_encryption-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 7f5059c35e2feac69ece6d6a6274e3b21b2fd575c09a713a1b179b8b2147281a
MD5 3ad234560eb89169e154f799571cb93c
BLAKE2b-256 a3ff1c20c02c71404ce57619a0ebf731f5ec9278f9a285de3409ae548c6caffb

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page