From 5c00880dd1bf10107fd9bc0e8907046ef76e8c0b Mon Sep 17 00:00:00 2001 From: naohide anahara <57.x.mas@gmail.com> Date: Fri, 6 May 2022 09:38:20 +0900 Subject: [PATCH 1/4] Fix #10 --- encrypted_fields/fields.py | 4 +++- setup.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/encrypted_fields/fields.py b/encrypted_fields/fields.py index 26e8b16..4ef0bba 100644 --- a/encrypted_fields/fields.py +++ b/encrypted_fields/fields.py @@ -94,7 +94,9 @@ class EncryptedDateTimeField(EncryptedFieldMixin, models.DateTimeField): class EncryptedIntegerField(EncryptedFieldMixin, models.IntegerField): - pass + @cached_property + def validators(self): + return [*self.default_validators, *self._validators] class EncryptedDateField(EncryptedFieldMixin, models.DateField): 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", From 9ec3563bfa1d27803004776b0bded64670da13cd Mon Sep 17 00:00:00 2001 From: naohide anahara <57.x.mas@gmail.com> Date: Fri, 6 May 2022 22:38:10 +0900 Subject: [PATCH 2/4] Fix EncryptedIntegerField --- encrypted_fields/fields.py | 17 ++++++++++++++++- package_test/tests.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/encrypted_fields/fields.py b/encrypted_fields/fields.py index 4ef0bba..8e4dc2b 100644 --- a/encrypted_fields/fields.py +++ b/encrypted_fields/fields.py @@ -1,9 +1,12 @@ import base64 -from django.conf import settings +from django.utils import timezone + +import warnings from cryptography.fernet import Fernet, MultiFernet 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.db import models from django.utils.functional import cached_property @@ -94,6 +97,18 @@ class EncryptedDateTimeField(EncryptedFieldMixin, models.DateTimeField): class EncryptedIntegerField(EncryptedFieldMixin, models.IntegerField): + def get_prep_value(self, value): + if value is None: + return None + try: + value = int(value) + except (TypeError, ValueError) as e: + raise e.__class__( + "Field '%s' expected a number but got %r." % (self.name, value), + ) from e + else: + return super().get_prep_value(value) + @cached_property def validators(self): return [*self.default_validators, *self._validators] diff --git a/package_test/tests.py b/package_test/tests.py index 4372e4a..9e9c2c2 100644 --- a/package_test/tests.py +++ b/package_test/tests.py @@ -62,6 +62,11 @@ class FieldTest(TestCase): fresh_model = TestModel.objects.get(id=model.id) self.assertEqual(fresh_model.datetime, plaintext) + plaintext = "text" + + model.datetime = plaintext + model.save() + def test_integer_field_encrypted(self): plaintext = 42 @@ -77,6 +82,12 @@ class FieldTest(TestCase): fresh_model = TestModel.objects.get(id=model.id) self.assertEqual(fresh_model.integer, plaintext) + plaintext = "text" + + with self.assertRaises(ValueError): + model.integer = plaintext + model.save() + def test_date_field_encrypted(self): plaintext = timezone.now().date() @@ -90,6 +101,11 @@ class FieldTest(TestCase): self.assertNotEqual(ciphertext, plaintext.isoformat()) self.assertEqual(fresh_model.date, plaintext) + plaintext = "text" + + model.date = plaintext + model.save() + def test_float_field_encrypted(self): plaintext = 42.44 @@ -105,6 +121,11 @@ class FieldTest(TestCase): fresh_model = TestModel.objects.get(id=model.id) self.assertEqual(fresh_model.floating, plaintext) + plaintext = "text" + + model.floating = plaintext + model.save() + def test_email_field_encrypted(self): plaintext = "test@gmail.com" @@ -120,6 +141,11 @@ class FieldTest(TestCase): fresh_model = TestModel.objects.get(id=model.id) self.assertEqual(fresh_model.email, plaintext) + plaintext = "text" + + model.email = plaintext + model.save() + def test_boolean_field_encrypted(self): plaintext = True @@ -140,6 +166,11 @@ class FieldTest(TestCase): fresh_model = TestModel.objects.get(id=model.id) self.assertEqual(fresh_model.boolean, plaintext) + plaintext = "text" + + model.boolean = plaintext + model.save() + class RotatedSaltTestCase(TestCase): @classmethod From 025afa1987e10495399c3c0ce28565f245a96a71 Mon Sep 17 00:00:00 2001 From: naohide anahara <57.x.mas@gmail.com> Date: Fri, 6 May 2022 22:55:52 +0900 Subject: [PATCH 3/4] Fix get_prep_value and to_python function --- encrypted_fields/fields.py | 22 ++++++++-------------- package_test/tests.py | 21 +++++++++++++-------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/encrypted_fields/fields.py b/encrypted_fields/fields.py index 8e4dc2b..56acc52 100644 --- a/encrypted_fields/fields.py +++ b/encrypted_fields/fields.py @@ -2,7 +2,7 @@ import base64 from django.utils import timezone import warnings -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 @@ -49,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) @@ -70,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): @@ -97,18 +103,6 @@ class EncryptedDateTimeField(EncryptedFieldMixin, models.DateTimeField): class EncryptedIntegerField(EncryptedFieldMixin, models.IntegerField): - def get_prep_value(self, value): - if value is None: - return None - try: - value = int(value) - except (TypeError, ValueError) as e: - raise e.__class__( - "Field '%s' expected a number but got %r." % (self.name, value), - ) from e - else: - return super().get_prep_value(value) - @cached_property def validators(self): return [*self.default_validators, *self._validators] diff --git a/package_test/tests.py b/package_test/tests.py index 9e9c2c2..55ebc17 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 @@ -64,8 +65,9 @@ class FieldTest(TestCase): plaintext = "text" - model.datetime = plaintext - model.save() + with self.assertRaises(ValidationError): + model.datetime = plaintext + model.save() def test_integer_field_encrypted(self): plaintext = 42 @@ -103,8 +105,9 @@ class FieldTest(TestCase): plaintext = "text" - model.date = plaintext - model.save() + with self.assertRaises(ValidationError): + model.date = plaintext + model.save() def test_float_field_encrypted(self): plaintext = 42.44 @@ -123,8 +126,9 @@ class FieldTest(TestCase): plaintext = "text" - model.floating = plaintext - model.save() + with self.assertRaises(ValueError): + model.floating = plaintext + model.save() def test_email_field_encrypted(self): plaintext = "test@gmail.com" @@ -168,8 +172,9 @@ class FieldTest(TestCase): plaintext = "text" - model.boolean = plaintext - model.save() + with self.assertRaises(ValidationError): + model.boolean = plaintext + model.save() class RotatedSaltTestCase(TestCase): From 8c6563c0f3ce70ea1fbeb354f5f4b90fc732c034 Mon Sep 17 00:00:00 2001 From: naohide anahara <57.x.mas@gmail.com> Date: Sat, 7 May 2022 08:23:24 +0900 Subject: [PATCH 4/4] Fix EncryptedIntegerField and testcase --- encrypted_fields/fields.py | 37 ++++++++++++++++++++++++++++++++++--- package_test/tests.py | 29 ++++++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/encrypted_fields/fields.py b/encrypted_fields/fields.py index 56acc52..53cec54 100644 --- a/encrypted_fields/fields.py +++ b/encrypted_fields/fields.py @@ -1,13 +1,13 @@ import base64 -from django.utils import timezone -import warnings 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 @@ -105,7 +105,38 @@ class EncryptedDateTimeField(EncryptedFieldMixin, models.DateTimeField): class EncryptedIntegerField(EncryptedFieldMixin, models.IntegerField): @cached_property def validators(self): - return [*self.default_validators, *self._validators] + # 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 55ebc17..1bce167 100644 --- a/package_test/tests.py +++ b/package_test/tests.py @@ -23,6 +23,7 @@ class FieldTest(TestCase): model = TestModel() model.char = plaintext + model.full_clean() model.save() ciphertext = self.get_db_value("char", model.id) @@ -38,6 +39,7 @@ class FieldTest(TestCase): model = TestModel() model.text = plaintext + model.full_clean() model.save() ciphertext = self.get_db_value("text", model.id) @@ -53,6 +55,7 @@ class FieldTest(TestCase): model = TestModel() model.datetime = plaintext + model.full_clean() model.save() ciphertext = self.get_db_value("datetime", model.id) @@ -67,6 +70,7 @@ class FieldTest(TestCase): with self.assertRaises(ValidationError): model.datetime = plaintext + model.full_clean() model.save() def test_integer_field_encrypted(self): @@ -74,6 +78,7 @@ class FieldTest(TestCase): model = TestModel() model.integer = plaintext + model.full_clean() model.save() ciphertext = self.get_db_value("integer", model.id) @@ -84,10 +89,19 @@ 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(ValueError): + with self.assertRaises(TypeError): model.integer = plaintext + model.full_clean() model.save() def test_date_field_encrypted(self): @@ -95,6 +109,7 @@ class FieldTest(TestCase): model = TestModel() model.date = plaintext + model.full_clean() model.save() ciphertext = self.get_db_value("date", model.id) @@ -107,6 +122,7 @@ class FieldTest(TestCase): with self.assertRaises(ValidationError): model.date = plaintext + model.full_clean() model.save() def test_float_field_encrypted(self): @@ -114,6 +130,7 @@ class FieldTest(TestCase): model = TestModel() model.floating = plaintext + model.full_clean() model.save() ciphertext = self.get_db_value("floating", model.id) @@ -128,6 +145,7 @@ class FieldTest(TestCase): with self.assertRaises(ValueError): model.floating = plaintext + model.full_clean() model.save() def test_email_field_encrypted(self): @@ -135,6 +153,7 @@ class FieldTest(TestCase): model = TestModel() model.email = plaintext + model.full_clean() model.save() ciphertext = self.get_db_value("email", model.id) @@ -147,14 +166,17 @@ class FieldTest(TestCase): plaintext = "text" - model.email = plaintext - model.save() + 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) @@ -174,6 +196,7 @@ class FieldTest(TestCase): with self.assertRaises(ValidationError): model.boolean = plaintext + model.full_clean() model.save()