Django Tink Fields: encrypted models without ceremony

Django Tink Fields started as a practical itch: I wanted field-level encryption that felt like native Django. No custom ORM, no middleware labyrinth, just a field swap and real cryptography. The result is django-tink-fields, a small library that wraps Google Tink with Django model fields. The code lives at github.com/script3r/django-tink-fields, with the README and examples anchored in the repo (README, example_project).

The goal: boring encryption

The design target was boring, not flashy: let me replace CharField with EncryptedCharField and keep everything else the same. That meant shipping a wide spread of field types (text, email, JSON, dates, UUIDs, binary) and making sure validators still behave like the original fields.

Under the hood, everything stores bytes, and the field converts to and from Python types at the edges. If you don’t think about the crypto most days, that’s the point.

Keysets as a first-class contract

The configuration is deliberately explicit. You define keysets in settings, and every field points at one by name. A KeysetManager validates that setup early and caches handles so each request doesn’t re-open files or build new primitives.

TINK_FIELDS_CONFIG = {
    "default": {
        "cleartext": True,
        "path": "/path/to/dev_keyset.json",
    },
    "sensitive": {
        "cleartext": False,
        "path": "/path/to/prod_keyset.json",
        "master_key_aead": kms_aead,
    },
}

That split between cleartext keysets (dev) and encrypted keysets (prod) makes it easier to do the right thing without hiding the sharp edges.

Usage, end to end

Install the package, add it to your Django settings, and wire fields like any other model type.

pip install django-tink-fields
# settings.py
INSTALLED_APPS = [
    # ...
    "tink_fields",
]

TINK_FIELDS_CONFIG = {
    "default": {
        "cleartext": True,
        "path": "/path/to/dev_keyset.json",
    },
    "deterministic": {
        "cleartext": True,
        "path": "/path/to/dev_det_keyset.json",
    },
}
# models.py
from django.db import models
from tink_fields import (
    EncryptedCharField,
    EncryptedEmailField,
    EncryptedJSONField,
    DeterministicEncryptedEmailField,
)


class Customer(models.Model):
    name = EncryptedCharField(max_length=200, keyset="default")
    email = DeterministicEncryptedEmailField(keyset="deterministic")
    profile = EncryptedJSONField(keyset="default")
python manage.py makemigrations
python manage.py migrate
# usage in queries
Customer.objects.create(
    name="Ada Lovelace",
    email="ada@example.com",
    profile={"plan": "pro", "seat_count": 3},
)

Customer.objects.get(email="ada@example.com")

The repo includes a working sample app you can run locally if you prefer to see it in context: example_project.

Searchability, but only when you ask for it

Regular AEAD fields refuse lookups because ciphertext isn’t searchable. For the narrow cases where you need equality filters, the library adds deterministic fields that use Tink’s deterministic AEAD. It’s an explicit tradeoff, and the API makes it hard to pretend otherwise.

class Customer(models.Model):
    email = DeterministicEncryptedEmailField(keyset="deterministic")

Only exact and isnull lookups are supported. Everything else raises a hard error, which keeps the boundary honest.

AAD for contextual binding

Each field accepts an aad_callback, so you can bind encryption to model or field context. The default is empty bytes, but you can make AAD include the model label, field name, or tenant id. That makes ciphertext harder to replay across contexts without complicating the developer workflow.

def aad_for_customer(instance, field_name):
    return f"{instance._meta.label}:{field_name}".encode("utf-8")


class Customer(models.Model):
    name = EncryptedCharField(
        max_length=200,
        keyset="default",
        aad_callback=aad_for_customer,
    )

Tests that act like a real app

The repo ships a minimal Django project under example_project/ to exercise real model usage, ciphertext at rest, deterministic lookups, and tamper detection. The unit tests validate keyset config failures, keyset caching, and the deterministic behavior that must stay predictable.

The project is small on purpose, but I wanted it to feel like a production-safe baseline: readable, explicit, and hard to misuse without being noisy.