FossilRepo
| c588255… | ragelink | 1 | """Custom model fields — encrypted storage using Fernet symmetric encryption.""" |
| c588255… | ragelink | 2 | |
| c588255… | ragelink | 3 | import base64 |
| c588255… | ragelink | 4 | import hashlib |
| c588255… | ragelink | 5 | |
| c588255… | ragelink | 6 | from cryptography.fernet import Fernet, InvalidToken |
| c588255… | ragelink | 7 | from django.conf import settings |
| c588255… | ragelink | 8 | from django.db import models |
| c588255… | ragelink | 9 | |
| c588255… | ragelink | 10 | |
| c588255… | ragelink | 11 | def _get_fernet(): |
| c588255… | ragelink | 12 | """Derive a Fernet key from Django's SECRET_KEY.""" |
| c588255… | ragelink | 13 | key_bytes = hashlib.sha256(settings.SECRET_KEY.encode()).digest() |
| c588255… | ragelink | 14 | return Fernet(base64.urlsafe_b64encode(key_bytes)) |
| c588255… | ragelink | 15 | |
| c588255… | ragelink | 16 | |
| c588255… | ragelink | 17 | class EncryptedTextField(models.TextField): |
| c588255… | ragelink | 18 | """TextField that encrypts data at rest using Fernet (AES-128-CBC + HMAC). |
| c588255… | ragelink | 19 | |
| c588255… | ragelink | 20 | Values are transparently encrypted on save and decrypted on read. |
| c588255… | ragelink | 21 | Stored as base64-encoded ciphertext in the database. |
| c588255… | ragelink | 22 | """ |
| c588255… | ragelink | 23 | |
| c588255… | ragelink | 24 | def get_prep_value(self, value): |
| c588255… | ragelink | 25 | if value is None or value == "": |
| c588255… | ragelink | 26 | return value |
| c588255… | ragelink | 27 | f = _get_fernet() |
| c588255… | ragelink | 28 | return f.encrypt(value.encode("utf-8")).decode("utf-8") |
| c588255… | ragelink | 29 | |
| c588255… | ragelink | 30 | def from_db_value(self, value, expression, connection): |
| c588255… | ragelink | 31 | if value is None or value == "": |
| c588255… | ragelink | 32 | return value |
| c588255… | ragelink | 33 | f = _get_fernet() |
| c588255… | ragelink | 34 | try: |
| c588255… | ragelink | 35 | return f.decrypt(value.encode("utf-8")).decode("utf-8") |
| c588255… | ragelink | 36 | except InvalidToken: |
| c588255… | ragelink | 37 | # Value may not be encrypted (e.g. pre-existing data). |
| c588255… | ragelink | 38 | return value |
| c588255… | ragelink | 39 | |
| c588255… | ragelink | 40 | def deconstruct(self): |
| c588255… | ragelink | 41 | name, path, args, kwargs = super().deconstruct() |
| c588255… | ragelink | 42 | return name, path, args, kwargs |