|
1 | 1 | Encrypted Model Fields |
2 | 2 | ====================== |
3 | 3 |
|
4 | | -If your ``custom_code`` or reusable app contains a Model field storing user-specific |
5 | | -sensitive data, then you may want to encrypt that data. |
| 4 | +If your ``custom_code`` or reusable app contains a model field storing |
| 5 | +user-specific sensitive data, you may want to encrypt that data at rest. |
| 6 | + |
| 7 | +Examples of user-specific sensitive data include passwords or API keys |
| 8 | +for external services that your TOM uses on the user's behalf. TOM |
| 9 | +Toolkit's Facility modules, for example, use the mechanism described |
| 10 | +here to store user-specific external-service credentials in a user |
| 11 | +profile model. Real examples live in |
| 12 | +`tom_eso <https://github.com/TOMToolkit/tom_eso>`__ and |
| 13 | +`tom_swift <https://github.com/TOMToolkit/tom_swift>`__. |
| 14 | + |
| 15 | +How encryption works |
| 16 | +-------------------- |
| 17 | + |
| 18 | +Encrypted fields are protected by a single Fernet cipher derived from |
| 19 | +``settings.SECRET_KEY`` using HKDF (RFC 5869) with a domain-separator |
| 20 | +label. There is no per-user key material and no additional environment |
| 21 | +variable to manage. For the operator-side concerns (rotating |
| 22 | +``SECRET_KEY`` without losing data, etc.) see |
| 23 | +:doc:`/deployment/encryption`. |
| 24 | + |
| 25 | +.. note:: |
| 26 | + |
| 27 | + Encryption protects data from passive database exposure. It does NOT |
| 28 | + protect against a server administrator with access to ``SECRET_KEY`` |
| 29 | + — by design, since the same admin needs to be able to run the |
| 30 | + ``rotate_encryption_key`` command. If you need user-level isolation |
| 31 | + from administrators, the toolkit's current scheme is not sufficient. |
| 32 | + |
| 33 | +Adding an encrypted field to a model |
| 34 | +------------------------------------ |
| 35 | + |
| 36 | +Two pieces are needed on the model: a ``BinaryField`` for the |
| 37 | +ciphertext, and an :class:`EncryptedProperty` descriptor that handles |
| 38 | +encryption on write and decryption on read. |
6 | 39 |
|
7 | | -Examples of user-specific sensitive |
8 | | -data include a password or API key for an external service that your TOM uses. |
9 | | -For example, TOMToolkit Facility modules can use the mechanism described here to store, |
10 | | -encrypted, user-specific credentials in a user profile model. Examples include the |
11 | | -`tom_eso <https://github.com/TOMToolkit/tom_eso>`__ and the |
12 | | -`tom_swift <https://github.com/TOMToolkit/tom_swift>`__ facility modules. |
13 | | - |
14 | | -As we explain below, TOMToolkit provides a *mix-in* class, a *property descriptor*, and |
15 | | -utility functions to help encrypt user-specific sensitive data and access it when it's needed. |
16 | | - |
17 | | -.. note:: For sensitive data that is used by the TOM itself and is not user-specific, |
18 | | - we suggest that this data be stored outside the TOM and accessed through |
19 | | - environment variables. |
20 | | - |
21 | | -Creating and accessing an encrypted Model field |
22 | | ------------------------------------------------ |
| 40 | +.. code-block:: python |
23 | 41 |
|
24 | | -Creating an encrypted Model field |
25 | | -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 42 | + from django.conf import settings |
| 43 | + from django.db import models |
| 44 | + from tom_common.models import EncryptedProperty |
26 | 45 |
|
27 | | -If your Model has a field that should be encrypted, follow these steps: |
28 | 46 |
|
29 | | -1. Import the mix-in class and property descriptor in your ``models.py``: |
| 47 | + class MyAppProfile(models.Model): |
| 48 | + user = models.OneToOneField(settings.AUTH_USER_MODEL, |
| 49 | + on_delete=models.CASCADE) |
| 50 | + _api_key_encrypted = models.BinaryField(null=True, blank=True) # ciphertext (private) |
| 51 | + api_key = EncryptedProperty('_api_key_encrypted') # descriptor (public) |
30 | 52 |
|
31 | | -.. code-block:: python |
| 53 | +By convention, the ``BinaryField``'s name starts with an underscore — |
| 54 | +its only consumer is the :class:`EncryptedProperty` descriptor; plugin |
| 55 | +code should never read or write it directly. |
32 | 56 |
|
33 | | - from tom_common.models import EncryptableModelMixin, EncryptedProperty |
| 57 | +Reading and writing the field |
| 58 | +----------------------------- |
34 | 59 |
|
35 | | -2. Make your Model subclass a subclass of ``EcryptableModelMixin``. For example: |
| 60 | +Plain Python attribute access on the descriptor handles everything: |
36 | 61 |
|
37 | 62 | .. code-block:: python |
38 | 63 |
|
39 | | - class MyAppModel(EncryptableModelMixin, models.Model): |
40 | | - ... |
| 64 | + profile.api_key = 'something-secret' |
| 65 | + profile.save() |
41 | 66 |
|
42 | | -This gives your model access to a set of methods that will manage the encryption and |
43 | | -decryption of your data into and out of the ``BinaryField`` that stores the encrypted data. |
| 67 | + # later, possibly in a different process / request: |
| 68 | + value = profile.api_key # 'something-secret' |
44 | 69 |
|
45 | | -3. Add the ``BinaryField`` that will store the encrypted data and the property descriptor |
46 | | -through which the ``BinaryField`` will be accessed. |
| 70 | +On assignment, the descriptor calls |
| 71 | +:func:`tom_common.encryption.encrypt`, which builds a Fernet cipher |
| 72 | +from ``settings.SECRET_KEY`` and encrypts the value, then stores the |
| 73 | +ciphertext bytes in the underlying ``BinaryField``. On read, the |
| 74 | +descriptor calls :func:`tom_common.encryption.decrypt`, which |
| 75 | +transparently honours ``settings.SECRET_KEY_FALLBACKS`` (see |
| 76 | +:doc:`/deployment/encryption` for the rotation procedure). |
47 | 77 |
|
48 | | -.. code-block:: python |
| 78 | +An empty string assignment clears the ciphertext (stores ``None`` in |
| 79 | +the ``BinaryField``). Reading an unset / empty field yields ``''``, |
| 80 | +not ``None`` — so consumers don't need to special-case the empty case. |
49 | 81 |
|
50 | | - _ciphertext_api_key = BinaryField(null=True, blank=True) # encrypted data field (private) |
51 | | - api_key = EncryptedProperty('_ciphertext_api_key') # descriptor that provides access (public) |
| 82 | +Some explanations |
| 83 | +----------------- |
52 | 84 |
|
53 | | -By convention name of the ``BinaryField`` field should begin with and underscore |
54 | | -(``_ciphertext_api_key`` in our example) because is it private to the Model class. |
| 85 | +:class:`EncryptedProperty` (`source <https://github.com/TOMToolkit/tom_base/blob/dev/tom_common/models.py>`__) |
| 86 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
55 | 87 |
|
56 | | -Accessing encrypted data |
57 | | -~~~~~~~~~~~~~~~~~~~~~~~~ |
58 | | -The following example shows how to get and set an encrypted field using the utility |
59 | | -methods provided in ``tom_common.session_utils.py``: |
| 88 | +A *property descriptor* implementing the Python descriptor protocol |
| 89 | +(``__get__``, ``__set__``, ``__set_name__``). It handles the details of |
| 90 | +decrypting the ciphertext ``BinaryField`` on its way out of the |
| 91 | +database and encrypting it on the way in. It is invoked whenever the |
| 92 | +property is accessed (e.g. ``profile.api_key`` reads; |
| 93 | +``profile.api_key = 'x'`` writes). |
60 | 94 |
|
61 | | -.. code-block:: python |
62 | | -
|
63 | | - from tom_common.session_utils import get_encrypted_field, set_encrypted_field |
64 | | - from tom_app_example.models import MyAppModel |
65 | | - |
66 | | - profile: MyAppModel = user.myappmodel # Model instance containing an encrypted field |
67 | | - |
68 | | - # getter example |
69 | | - decrypted_api_key: str = get_encrypted_field(user, profile, 'api_key') |
70 | | - |
71 | | - # setter example |
72 | | - new_api_key: str = 'something_secret' |
73 | | - set_encrypted_field(user, profile, 'api_key', new_api_key) |
74 | | -
|
75 | | -Note here that the User instance (``user``) is used to access the ``EncryptableModelMixin`` |
76 | | -subclass and its encrypted data. The ``user`` property of the Model subclass containing the |
77 | | -encrypted field (``MyAppModel`` in our example) is provided by the ``EncryptableModelMixin``. |
78 | | -As such, the model *should not define a* ``user`` *property of its own*. |
79 | | - |
80 | | -Some Explanations |
81 | | ------------------ |
| 95 | +The descriptor reads the cipher from |
| 96 | +:func:`tom_common.encryption._get_cipher` on every access, so changes |
| 97 | +to ``settings.SECRET_KEY`` (via ``override_settings`` in tests, or a |
| 98 | +deployment-level rotation) take effect immediately on the next read. |
82 | 99 |
|
83 | | -EncryptableModelMixin |
84 | | -~~~~~~~~~~~~~~~~~~~~~ |
85 | | -An abstract Django model mixin that provides a standardized ``user`` OneToOneField. |
86 | | -Any model that stores encrypted data via ``EncryptedProperty`` should inherit from |
87 | | -this mixin. The ``user`` field ties encrypted data to its owner, allowing the |
88 | | -helper functions in ``session_utils`` to look up the user's Data Encryption Key |
89 | | -(DEK) and build the cipher needed for encryption and decryption. |
90 | | - |
91 | | -EncryptedProperty (`source <https://github.com/TOMToolkit/tom_base/blob/069024f954e5540c1441c5186378de538f7d606f/tom_common/models.py#L39>`__) |
92 | | -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
93 | | -A *property descriptor* implements the Python descriptor protocol (``__get__``, |
94 | | -``__set__``, etc). The ``EncryptedProperty`` property descriptor handles the details |
95 | | -of decrypting the encrypted ``BinaryField`` on its way out of the database and |
96 | | -encrypting it on the way in. It is invoked when the property is accessed |
97 | | -(e.g. ``model_instance.api_key``). |
98 | | - |
99 | | -Session Utils (`example <https://github.com/TOMToolkit/tom_eso/blob/b74fe3b951ead6f6f332594724731d036944da47/tom_eso/eso.py#L209>`__) |
100 | | -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
101 | | -The ``get_encrypted_field`` and ``set_encrypted_field`` functions implement |
102 | | -boilerplate code for creating and destroying the cipher used to encrypt and |
103 | | -decrypt the ``BinaryField``. *These methods must always be used to access any |
104 | | -encrypted field*. |
105 | | - |
106 | | - |
107 | | -The rest of the details are in the source code. If reading source code isn't your thing, |
108 | | -please do feel free to get in touch and we'll be happy to answer any questions you may have. |
| 100 | +The rest of the details are in the source. If reading source isn't |
| 101 | +your thing, feel free to get in touch and we'll be happy to answer |
| 102 | +questions. |
0 commit comments