Welcome to django-fernet-fields!¶
Fernet symmetric encryption for Django model fields, using the cryptography library.
Prerequisites¶
django-fernet-fields
supports Django 1.8.2 and later on Python 2.7, 3.3,
3.4, pypy, and pypy3.
Only PostgreSQL, SQLite, and MySQL are tested, but any Django database backend
with support for BinaryField
should work.
Installation¶
django-fernet-fields
is available on PyPI. Install it with:
pip install django-fernet-fields
Usage¶
Just import and use the included field classes in your models:
from django.db import models
from fernet_fields import EncryptedTextField
class MyModel(models.Model):
name = EncryptedTextField()
You can assign values to and read values from the name
field as usual, but
the values will automatically be encrypted before being sent to the database
and decrypted when read from the database.
Encryption and decryption are performed in your app; the secret key is never sent to the database server. The database sees only the encrypted value of this field.
Field types¶
Several other field classes are included: EncryptedCharField
,
EncryptedEmailField
, EncryptedIntegerField
, EncryptedDateField
, and
EncryptedDateTimeField
. All field classes accept the same arguments as
their non-encrypted versions.
To create an encrypted version of some other custom field class, inherit from
both EncryptedField
and the other field class:
from fernet_fields import EncryptedField
from somewhere import MyField
class MyEncryptedField(EncryptedField, MyField):
pass
Nullable fields¶
Nullable encrypted fields are allowed; a None
value in Python is translated
to a real NULL
in the database column. Note that this trivially reveals the
presence or absence of data in the column to an attacker. If this is a problem
for your case, avoid using a nullable encrypted field; instead store some other
sentinel “empty” value (which will be encrypted just like any other value) in a
non-nullable encrypted field.
Keys¶
By default, django-fernet-fields
uses your SECRET_KEY
setting as the
encryption key.
You can instead provide a list of keys in the FERNET_KEYS
setting; the
first key will be used to encrypt all new data, and decryption of existing
values will be attempted with all given keys in order. This is useful for key
rotation: place a new key at the head of the list for use with all new or
changed data, but existing values encrypted with old keys will still be
accessible:
FERNET_KEYS = [
'new key for encrypting',
'older key for decrypting old data',
]
Warning
Once you start saving data using a given encryption key (whether your
SECRET_KEY
or another key), don’t lose track of that key or you will
lose access to all data encrypted using it! And keep the key secret; anyone
who gets ahold of it will have access to all your encrypted data.
Disabling HKDF¶
Fernet encryption requires a 32-bit url-safe base-64 encoded secret key. By
default, django-fernet-fields
uses HKDF to derive such a key from
whatever arbitrary secret key you provide.
If you wish to disable HKDF and provide your own Fernet-compatible 32-bit
key(s) (e.g. generated with Fernet.generate_key()) directly instead, just
set FERNET_USE_HKDF = False
in your settings file. If this is set, all keys
specified in the FERNET_KEYS
setting must be 32-bit and url-safe
base64-encoded bytestrings. If a key is not in the correct format, you’ll
likely get “incorrect padding” errors.
Warning
If you don’t define a FERNET_KEYS
setting, your SECRET_KEY
setting
is the fallback key. If you disable HKDF, this means that your
SECRET_KEY
itself needs to be a Fernet-compatible key.
Indexes, constraints, and lookups¶
Because Fernet encryption is not deterministic (the same source text encrypted using the same key will result in a different encrypted token each time), indexing or enforcing uniqueness or performing lookups against encrypted data is useless. Every encrypted value will always be different, and every exact-match lookup will fail; other lookups’ results would be meaningless.
For this reason, EncryptedField
will raise
django.core.exceptions.ImproperlyConfigured
if passed any of
db_index=True
, unique=True
, or primary_key=True
, and any type of
lookup on an EncryptedField
except for isnull
will raise
django.core.exceptions.FieldError
.
Ordering¶
Ordering a queryset by an EncryptedField
will not raise an error, but it
will order according to the encrypted data, not the decrypted value, which is
not very useful and probably not desired.
Raising an error would be better, but there’s no mechanism in Django for a
field class to declare that it doesn’t support ordering. It could be done
easily enough with a custom queryset and model manager that overrides
order_by()
to check the supplied field names. You might consider doing this
for your models, if you’re concerned that you might accidentally order by an
EncryptedField
and get junk ordering without noticing.
Migrations¶
If migrating an existing non-encrypted field to its encrypted counterpart, you
won’t be able to use a simple AlterField
operation. Since your database has
no access to the encryption key, it can’t update the column values
correctly. Instead, you’ll need to do a three-step migration dance:
- Add the new encrypted field with a different name and initialize its values as null, otherwise decryption will be attempted before anything has been encrypted.
- Write a data migration (using RunPython and the ORM, not raw SQL) to copy the values from the old field to the new (which automatically encrypts them in the process).
- Remove the old field and (if needed) rename the new encrypted field to the old field’s name.
Note about deploying to Heroku¶
An important caveat when deploying an app dependent of Fernet to Heroku: you need to specify all requirements (even dependencies of dependencies) explicitly. In general, this is a good practice for version pinning purposes. But it’s necessary for Fernet on Heroku because it depends on cryptography library, which in turn depends on libffi, a C library. When cryptography is explicitly defined on requirements.txt, Heroku knows it depends on libffi and installs it.
Therefore, an easy solution is to freeze your requirements after installing Fernet:
pip freeze > requirements.txt
Contributing¶
See the contributing docs.