Encrypted Model Fields#
If your custom_code or reusable app needs to store a secret (e.g. an API key
or password for an external service) — TOM Toolkit provides
EncryptedModelField, a model field that
encrypts its value as it is stored in the database.
Note
Encryption protects the secret from passive database exposure. It does
not protect it from anyone who can read your TOM’s settings.SECRET_KEY
(such as a server administrator). See Encryption and the SECRET_KEY for the
encryption documentation relevant to TOM administrators.
This page describes how to add an encrypted field to a user-profile model, how to display it, and how to edit it. Working examples live in tom_hermes, tom_eso, and tom_demoapp.
Adding an EncryptedModelField#
Declare an EncryptedModelField on your model:
from django.conf import settings
from django.db import models
from tom_common.encryption import EncryptedModelField
class MyAppProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
api_key = EncryptedModelField(null=True, blank=True)
Read and write it like any other field; the value is encrypted on save and decrypted on load:
profile.api_key = 'something-secret' # encrypted on save
profile.save()
profile.api_key # -> 'something-secret' (decrypted on load)
Assigning None or '' clears the stored value (the column becomes
NULL); reading an unset value returns None.
Displaying the value of an encrypted field#
To show the value with a click-to-reveal control, use tom_common’s
revealable_password_input.html partial. Pass it the plaintext, which
comes from direct attribute access (profile.api_key):
context = {'api_key': profile.api_key} # plaintext becomes part of the template context
{% if api_key %}
{% include 'tom_common/partials/revealable_password_input.html' with value=api_key %}
{% else %}
(not set)
{% endif %}
Note
Pass the plaintext only to templates the current user is allowed to see: the
partial embeds the plaintext value in the page HTML. Revealing it merely toggles
its visibility. Do not source the value from model_to_dict, a DRF
serializer, or dumpdata — those return a REDACTED placeholder for
encrypted fields, never the secret. (If a profile card auto-iterates fields
with model_to_dict, exclude the encrypted one and add profile.api_key
back explicitly.)
Editing the value in an UpdateView#
List the field on a ModelForm-based view. It renders as a masked input
paired with a Clear checkbox:
class MyProfileUpdateView(UpdateView):
model = MyAppProfile
fields = ['api_key']
On submitting the update form, an EncryptedModelField has the following behavior:
a new, typed-in value replaces the stored one;
a blank input keeps the stored value — so editing other fields on the same form never wipes the secret;
Checking the Clear checkbox with a blank input removes the current value (column becomes
NULL);a typed-in value together with Clear keeps the typed value (Clear is ignored).
The form never renders the stored value, so it can’t leak through the edit
page. The placeholder text displayed in the input box signals the current state
— (A stored value is hidden) — type to replace versus (not set) — type to add.
How it works (implementation details)#
Each value is encrypted with a Fernet cipher derived from
settings.SECRET_KEY (via a key derivation function, HKDF).
Decryption also tries any settings.SECRET_KEY_FALLBACKS, facilitating key rotation —
see Encryption and the SECRET_KEY for the procedure and the rotate_encryption_key command.
If a stored value cannot be decrypted under any active key, reading it raises
cryptography.fernet.InvalidToken — usually because a key was dropped from
the rotation set before its data was re-encrypted.
Limitations#
No filtering. Fernet ciphertext is non-deterministic, so equality lookups can never match;
MyAppProfile.objects.filter(api_key=...)raisesFieldError. For a searchable secret, store a companion HMAC hash column and query that.
API reference#
EncryptedModelField(source)A
models.BinaryFieldsubclass that encrypts on save and decrypts on load. See the class docstring for the full method-level contract.EncryptedFormField(source)The form-side companion (masked input plus the blank-preserves-existing behavior).
ModelFormpicks it up automatically viaEncryptedModelField.formfield().