diff --git a/encrypted_fields/fields.py b/encrypted_fields/fields.py index 26e8b16..53cec54 100644 --- a/encrypted_fields/fields.py +++ b/encrypted_fields/fields.py @@ -1,10 +1,13 @@ import base64 -from django.conf import settings -from cryptography.fernet import Fernet, MultiFernet + +from cryptography.fernet import Fernet, MultiFernet, InvalidToken from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from django.conf import settings +from django.core import validators from django.db import models +from django.db.backends.base.operations import BaseDatabaseOperations from django.utils.functional import cached_property @@ -46,6 +49,7 @@ class EncryptedFieldMixin(object): return "TextField" def get_prep_value(self, value): + value = super().get_prep_value(value) if value: if not isinstance(value, str): value = str(value) @@ -67,7 +71,12 @@ class EncryptedFieldMixin(object): or hasattr(self, "_already_decrypted") ): return value - value = self.f.decrypt(bytes(value, "utf-8")).decode("utf-8") + try: + value = self.f.decrypt(bytes(value, "utf-8")).decode("utf-8") + except InvalidToken: + pass + except UnicodeEncodeError: + pass return super(EncryptedFieldMixin, self).to_python(value) def clean(self, value, model_instance): @@ -94,7 +103,40 @@ class EncryptedDateTimeField(EncryptedFieldMixin, models.DateTimeField): class EncryptedIntegerField(EncryptedFieldMixin, models.IntegerField): - pass + @cached_property + def validators(self): + # These validators can't be added at field initialization time since + # they're based on values retrieved from `connection`. + validators_ = [*self.default_validators, *self._validators] + internal_type = models.IntegerField().get_internal_type() + min_value, max_value = BaseDatabaseOperations.integer_field_ranges[internal_type] + if min_value is not None and not any( + ( + isinstance(validator, validators.MinValueValidator) + and ( + validator.limit_value() + if callable(validator.limit_value) + else validator.limit_value + ) + >= min_value + ) + for validator in validators_ + ): + validators_.append(validators.MinValueValidator(min_value)) + if max_value is not None and not any( + ( + isinstance(validator, validators.MaxValueValidator) + and ( + validator.limit_value() + if callable(validator.limit_value) + else validator.limit_value + ) + <= max_value + ) + for validator in validators_ + ): + validators_.append(validators.MaxValueValidator(max_value)) + return validators_ class EncryptedDateField(EncryptedFieldMixin, models.DateField): diff --git a/package_test/tests.py b/package_test/tests.py index 4372e4a..1bce167 100644 --- a/package_test/tests.py +++ b/package_test/tests.py @@ -3,6 +3,7 @@ import re from django.db import connection from django.test import TestCase, override_settings from django.utils import timezone +from django.core.exceptions import ValidationError from .models import TestModel @@ -22,6 +23,7 @@ class FieldTest(TestCase): model = TestModel() model.char = plaintext + model.full_clean() model.save() ciphertext = self.get_db_value("char", model.id) @@ -37,6 +39,7 @@ class FieldTest(TestCase): model = TestModel() model.text = plaintext + model.full_clean() model.save() ciphertext = self.get_db_value("text", model.id) @@ -52,6 +55,7 @@ class FieldTest(TestCase): model = TestModel() model.datetime = plaintext + model.full_clean() model.save() ciphertext = self.get_db_value("datetime", model.id) @@ -62,11 +66,19 @@ class FieldTest(TestCase): fresh_model = TestModel.objects.get(id=model.id) self.assertEqual(fresh_model.datetime, plaintext) + plaintext = "text" + + with self.assertRaises(ValidationError): + model.datetime = plaintext + model.full_clean() + model.save() + def test_integer_field_encrypted(self): plaintext = 42 model = TestModel() model.integer = plaintext + model.full_clean() model.save() ciphertext = self.get_db_value("integer", model.id) @@ -77,11 +89,27 @@ class FieldTest(TestCase): fresh_model = TestModel.objects.get(id=model.id) self.assertEqual(fresh_model.integer, plaintext) + # "IntegerField": (-2147483648, 2147483647) + plaintext = 2147483648 + + with self.assertRaises(ValidationError): + model.integer = plaintext + model.full_clean() + model.save() + + plaintext = "text" + + with self.assertRaises(TypeError): + model.integer = plaintext + model.full_clean() + model.save() + def test_date_field_encrypted(self): plaintext = timezone.now().date() model = TestModel() model.date = plaintext + model.full_clean() model.save() ciphertext = self.get_db_value("date", model.id) @@ -90,11 +118,19 @@ class FieldTest(TestCase): self.assertNotEqual(ciphertext, plaintext.isoformat()) self.assertEqual(fresh_model.date, plaintext) + plaintext = "text" + + with self.assertRaises(ValidationError): + model.date = plaintext + model.full_clean() + model.save() + def test_float_field_encrypted(self): plaintext = 42.44 model = TestModel() model.floating = plaintext + model.full_clean() model.save() ciphertext = self.get_db_value("floating", model.id) @@ -105,11 +141,19 @@ class FieldTest(TestCase): fresh_model = TestModel.objects.get(id=model.id) self.assertEqual(fresh_model.floating, plaintext) + plaintext = "text" + + with self.assertRaises(ValueError): + model.floating = plaintext + model.full_clean() + model.save() + def test_email_field_encrypted(self): plaintext = "test@gmail.com" model = TestModel() model.email = plaintext + model.full_clean() model.save() ciphertext = self.get_db_value("email", model.id) @@ -120,11 +164,19 @@ class FieldTest(TestCase): fresh_model = TestModel.objects.get(id=model.id) self.assertEqual(fresh_model.email, plaintext) + plaintext = "text" + + with self.assertRaises(ValidationError): + model.email = plaintext + model.full_clean() + model.save() + def test_boolean_field_encrypted(self): plaintext = True model = TestModel() model.boolean = plaintext + model.full_clean() model.save() ciphertext = self.get_db_value("boolean", model.id) @@ -140,6 +192,13 @@ class FieldTest(TestCase): fresh_model = TestModel.objects.get(id=model.id) self.assertEqual(fresh_model.boolean, plaintext) + plaintext = "text" + + with self.assertRaises(ValidationError): + model.boolean = plaintext + model.full_clean() + model.save() + class RotatedSaltTestCase(TestCase): @classmethod diff --git a/setup.py b/setup.py index ab12ced..4c8e8a4 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( author="fragment.co.jp", author_email="info@fragment.co.jp", packages=["encrypted_fields"], - version="0.1.1", + version="0.1.2", install_requires=[ "Django>=2.2", "cryptography>=35.0.0",