diff --git a/fernet_fields/__init__.py b/fernet_fields/__init__.py index 0d009e0..739ee5c 100644 --- a/fernet_fields/__init__.py +++ b/fernet_fields/__init__.py @@ -1,3 +1,3 @@ from .fields import * # noqa -__version__ = '0.5' +__version__ = '0.5.1.4' diff --git a/fernet_fields/fields.py b/fernet_fields/fields.py index 025da38..ee5bc8a 100644 --- a/fernet_fields/fields.py +++ b/fernet_fields/fields.py @@ -1,8 +1,12 @@ -from cryptography.fernet import Fernet, MultiFernet +import base64 +import os + +from cryptography.fernet import Fernet as OldFernet, MultiFernet +from cryptography.hazmat.backends import default_backend from django.conf import settings from django.core.exceptions import FieldError, ImproperlyConfigured from django.db import models -from django.utils.encoding import force_bytes, force_text +from django.utils.encoding import force_bytes, force_str from django.utils.functional import cached_property from . import hkdf @@ -16,9 +20,31 @@ 'EncryptedIntegerField', 'EncryptedDateField', 'EncryptedDateTimeField', + 'StrongerFernet', ] +class StrongerFernet(OldFernet): + # noinspection PyMissingConstructor + def __init__(self, key, backend=None): + if backend is None: + backend = default_backend() + + key = base64.urlsafe_b64decode(key) + if len(key) != 48: + raise ValueError( + "Fernet key must be 48 url-safe base64-encoded bytes." + ) + + self._signing_key = key[:16] + self._encryption_key = key[16:] + self._backend = backend + + @classmethod + def generate_key(cls): + return base64.urlsafe_b64encode(os.urandom(48)) + + class EncryptedField(models.Field): """A field that encrypts values using Fernet symmetric encryption.""" _internal_type = 'BinaryField' @@ -39,6 +65,7 @@ def __init__(self, *args, **kwargs): "%s does not support db_index=True." % self.__class__.__name__ ) + self.allowed_unencrypted_values = kwargs.pop('allowed_unencrypted_values', []) super(EncryptedField, self).__init__(*args, **kwargs) @cached_property @@ -57,8 +84,8 @@ def fernet_keys(self): @cached_property def fernet(self): if len(self.fernet_keys) == 1: - return Fernet(self.fernet_keys[0]) - return MultiFernet([Fernet(k) for k in self.fernet_keys]) + return StrongerFernet(self.fernet_keys[0]) + return MultiFernet([StrongerFernet(k) for k in self.fernet_keys]) def get_internal_type(self): return self._internal_type @@ -71,10 +98,11 @@ def get_db_prep_save(self, value, connection): retval = self.fernet.encrypt(force_bytes(value)) return connection.Database.Binary(retval) - def from_db_value(self, value, expression, connection, context): + def from_db_value(self, value, expression, connection, context=None): if value is not None: - value = bytes(value) - return self.to_python(force_text(self.fernet.decrypt(value))) + if value not in self.allowed_unencrypted_values: + value = self.fernet.decrypt(force_bytes(value)) + return self.to_python(force_str(value)) @cached_property def validators(self): diff --git a/fernet_fields/hkdf.py b/fernet_fields/hkdf.py index 5bb5baa..2147055 100644 --- a/fernet_fields/hkdf.py +++ b/fernet_fields/hkdf.py @@ -15,7 +15,7 @@ def derive_fernet_key(input_key): """Derive a 32-bit b64-encoded Fernet key from arbitrary input key.""" hkdf = HKDF( algorithm=hashes.SHA256(), - length=32, + length=48, salt=salt, info=info, backend=backend, diff --git a/fernet_fields/test/test_fields.py b/fernet_fields/test/test_fields.py index bd8e5b8..f519fa6 100644 --- a/fernet_fields/test/test_fields.py +++ b/fernet_fields/test/test_fields.py @@ -1,4 +1,3 @@ -from cryptography.fernet import Fernet from datetime import date, datetime from django.core.exceptions import FieldError, ImproperlyConfigured @@ -29,8 +28,8 @@ def test_key_rotation(self, settings): settings.FERNET_KEYS = ['key1', 'key2'] f = fields.EncryptedTextField() - enc1 = Fernet(f.fernet_keys[0]).encrypt(b'enc1') - enc2 = Fernet(f.fernet_keys[1]).encrypt(b'enc2') + enc1 = fields.StrongerFernet(f.fernet_keys[0]).encrypt(b'enc1') + enc2 = fields.StrongerFernet(f.fernet_keys[1]).encrypt(b'enc2') assert f.fernet.decrypt(enc1) == b'enc1' assert f.fernet.decrypt(enc2) == b'enc2' @@ -38,10 +37,10 @@ def test_key_rotation(self, settings): def test_no_hkdf(self, settings): """Can set FERNET_USE_HKDF=False to avoid HKDF.""" settings.FERNET_USE_HKDF = False - k1 = Fernet.generate_key() + k1 = fields.StrongerFernet.generate_key() settings.FERNET_KEYS = [k1] f = fields.EncryptedTextField() - fernet = Fernet(k1) + fernet = fields.StrongerFernet(k1) assert fernet.decrypt(f.fernet.encrypt(b'foo')) == b'foo' diff --git a/setup.py b/setup.py index 82ea323..00cb20a 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ def get_version(): setup( - name='django-fernet-fields', + name='singular-django-fernet-fields', version=get_version(), description="Fernet-encrypted model fields for Django", long_description=long_description,