import base64 import hashlib from cryptography.fernet import Fernet from django.conf import settings from django.db import models def _get_fernet(): """Derive a Fernet key from Django's SECRET_KEY.""" raw_key = hashlib.sha256(settings.SECRET_KEY.encode()).digest() return Fernet(base64.urlsafe_b64encode(raw_key)) class EncryptedCharField(models.TextField): """ Stores values encrypted at rest using Fernet symmetric encryption. The encryption key is derived from Django's SECRET_KEY so no extra secrets management is required — if the SECRET_KEY is set, values are protected. From the application's perspective this behaves like a plain text field: you read/write plaintext; encryption/decryption happens transparently. """ def from_db_value(self, value, expression, connection): if not value: return value try: return _get_fernet().decrypt(value.encode()).decode() except Exception: # Gracefully return raw value if decryption fails # (e.g. migrating plaintext rows that were saved before encryption) return value def get_prep_value(self, value): if not value: return value return _get_fernet().encrypt(value.encode()).decode()