mirror of
https://github.com/jazzband/django-fernet-encrypted-fields.git
synced 2026-03-16 22:40:27 +00:00
311 lines
8.9 KiB
Python
311 lines
8.9 KiB
Python
import json
|
|
import re
|
|
|
|
import pytest
|
|
from django.core.exceptions import ValidationError
|
|
from django.db import connection
|
|
from django.test import TestCase, override_settings
|
|
from django.utils import timezone
|
|
|
|
from .models import TestModel
|
|
|
|
|
|
class FieldTest(TestCase):
|
|
def get_db_value(self, field: str, model_id: int) -> None:
|
|
cursor = connection.cursor()
|
|
cursor.execute(
|
|
f"select {field} from package_test_testmodel where id = {model_id};"
|
|
)
|
|
return cursor.fetchone()[0]
|
|
|
|
def test_char_field_encrypted(self) -> None:
|
|
plaintext = "Oh hi, test reader!"
|
|
|
|
model = TestModel()
|
|
model.char = plaintext
|
|
model.full_clean()
|
|
model.save()
|
|
|
|
ciphertext = self.get_db_value("char", model.id)
|
|
|
|
assert plaintext != ciphertext
|
|
assert "test" not in ciphertext
|
|
|
|
fresh_model = TestModel.objects.get(id=model.id)
|
|
assert fresh_model.char == plaintext
|
|
|
|
def test_text_field_encrypted(self) -> None:
|
|
plaintext = "Oh hi, test reader!" * 10
|
|
|
|
model = TestModel()
|
|
model.text = plaintext
|
|
model.full_clean()
|
|
model.save()
|
|
|
|
ciphertext = self.get_db_value("text", model.id)
|
|
|
|
assert plaintext != ciphertext
|
|
assert "test" not in ciphertext
|
|
|
|
fresh_model = TestModel.objects.get(id=model.id)
|
|
assert fresh_model.text == plaintext
|
|
|
|
def test_datetime_field_encrypted(self) -> None:
|
|
plaintext = timezone.now()
|
|
|
|
model = TestModel()
|
|
model.datetime = plaintext
|
|
model.full_clean()
|
|
model.save()
|
|
|
|
ciphertext = self.get_db_value("datetime", model.id)
|
|
|
|
# Django's normal date serialization format
|
|
assert re.search(r"^\d\d\d\d-\d\d-\d\d", ciphertext) is None
|
|
|
|
fresh_model = TestModel.objects.get(id=model.id)
|
|
assert fresh_model.datetime == plaintext
|
|
|
|
plaintext = "text"
|
|
model.datetime = plaintext
|
|
model.full_clean()
|
|
|
|
with pytest.raises(ValidationError):
|
|
model.save()
|
|
|
|
def test_integer_field_encrypted(self) -> None:
|
|
plaintext = 42
|
|
|
|
model = TestModel()
|
|
model.integer = plaintext
|
|
model.full_clean()
|
|
model.save()
|
|
|
|
ciphertext = self.get_db_value("integer", model.id)
|
|
|
|
assert plaintext != ciphertext
|
|
assert plaintext != str(ciphertext)
|
|
|
|
fresh_model = TestModel.objects.get(id=model.id)
|
|
assert fresh_model.integer == plaintext
|
|
|
|
# "IntegerField": (-2147483648, 2147483647)
|
|
plaintext = 2147483648
|
|
model.integer = plaintext
|
|
|
|
with pytest.raises(ValidationError):
|
|
model.full_clean()
|
|
|
|
plaintext = "text"
|
|
model.integer = plaintext
|
|
|
|
with pytest.raises(TypeError):
|
|
model.full_clean()
|
|
|
|
def test_date_field_encrypted(self) -> None:
|
|
plaintext = timezone.now().date()
|
|
|
|
model = TestModel()
|
|
model.date = plaintext
|
|
model.full_clean()
|
|
model.save()
|
|
|
|
ciphertext = self.get_db_value("date", model.id)
|
|
fresh_model = TestModel.objects.get(id=model.id)
|
|
|
|
assert ciphertext != plaintext.isoformat()
|
|
assert fresh_model.date == plaintext
|
|
|
|
plaintext = "text"
|
|
model.date = plaintext
|
|
model.full_clean()
|
|
|
|
with pytest.raises(ValidationError):
|
|
model.save()
|
|
|
|
def test_float_field_encrypted(self) -> None:
|
|
plaintext = 42.44
|
|
|
|
model = TestModel()
|
|
model.floating = plaintext
|
|
model.full_clean()
|
|
model.save()
|
|
|
|
ciphertext = self.get_db_value("floating", model.id)
|
|
|
|
assert plaintext != ciphertext
|
|
assert plaintext != str(ciphertext)
|
|
|
|
fresh_model = TestModel.objects.get(id=model.id)
|
|
assert fresh_model.floating == plaintext
|
|
|
|
plaintext = "text"
|
|
model.floating = plaintext
|
|
model.full_clean()
|
|
|
|
with pytest.raises(ValueError):
|
|
model.save()
|
|
|
|
def test_email_field_encrypted(self) -> None:
|
|
plaintext = "test@gmail.com"
|
|
|
|
model = TestModel()
|
|
model.email = plaintext
|
|
model.full_clean()
|
|
model.save()
|
|
|
|
ciphertext = self.get_db_value("email", model.id)
|
|
|
|
assert plaintext != ciphertext
|
|
assert "aron" not in ciphertext
|
|
|
|
fresh_model = TestModel.objects.get(id=model.id)
|
|
assert fresh_model.email == plaintext
|
|
|
|
plaintext = "text"
|
|
model.email = plaintext
|
|
|
|
with pytest.raises(ValidationError):
|
|
model.full_clean()
|
|
|
|
def test_boolean_field_encrypted(self) -> None:
|
|
plaintext = True
|
|
|
|
model = TestModel()
|
|
model.boolean = plaintext
|
|
model.full_clean()
|
|
model.save()
|
|
|
|
ciphertext = self.get_db_value("boolean", model.id)
|
|
|
|
assert plaintext != ciphertext
|
|
assert ciphertext is not True
|
|
assert ciphertext != "True"
|
|
assert ciphertext != "true"
|
|
assert ciphertext != "1"
|
|
assert ciphertext != 1
|
|
assert not isinstance(ciphertext, bool)
|
|
|
|
fresh_model = TestModel.objects.get(id=model.id)
|
|
assert fresh_model.boolean == plaintext
|
|
|
|
plaintext = "text"
|
|
model.boolean = plaintext
|
|
model.full_clean()
|
|
|
|
with pytest.raises(ValidationError):
|
|
model.save()
|
|
|
|
def test_json_field_encrypted(self) -> None:
|
|
dict_values = {
|
|
"key": "value",
|
|
"list": ["nested", {"key": "val"}],
|
|
"nested": {"child": "sibling"},
|
|
}
|
|
|
|
model = TestModel()
|
|
model.json = dict_values
|
|
model.full_clean()
|
|
model.save()
|
|
|
|
ciphertext = json.loads(self.get_db_value("json", model.id))
|
|
|
|
assert dict_values != ciphertext
|
|
|
|
fresh_model = TestModel.objects.get(id=model.id)
|
|
assert fresh_model.json == dict_values
|
|
|
|
def test_json_field_retains_keys(self) -> None:
|
|
plain_value = {"key": "value", "another_key": "some value"}
|
|
|
|
model = TestModel()
|
|
model.json = plain_value
|
|
model.full_clean()
|
|
model.save()
|
|
|
|
ciphertext = json.loads(self.get_db_value("json", model.id))
|
|
|
|
assert plain_value.keys() == ciphertext.keys()
|
|
|
|
|
|
class RotatedSaltTestCase(TestCase):
|
|
@classmethod
|
|
@override_settings(SALT_KEY=["abcdefghijklmnopqrstuvwxyz0123456789"])
|
|
def setUpTestData(cls) -> None:
|
|
"""Create the initial record using the old salt"""
|
|
cls.original = TestModel.objects.create(text="Oh hi test reader")
|
|
|
|
@override_settings(SALT_KEY=["newkeyhere", "abcdefghijklmnopqrstuvwxyz0123456789"])
|
|
def test_rotated_salt(self) -> None:
|
|
"""Change the salt, keep the old one as the last in the list for reading"""
|
|
plaintext = "Oh hi test reader"
|
|
model = TestModel()
|
|
model.text = plaintext
|
|
model.save()
|
|
|
|
ciphertext = FieldTest.get_db_value(self, "text", model.id)
|
|
|
|
assert plaintext != ciphertext
|
|
assert "test" not in ciphertext
|
|
|
|
fresh_model = TestModel.objects.get(id=model.id)
|
|
assert fresh_model.text == plaintext
|
|
|
|
old_record = TestModel.objects.get(id=self.original.id)
|
|
assert fresh_model.text == old_record.text
|
|
|
|
assert ciphertext != FieldTest.get_db_value(self, "text", self.original.pk)
|
|
|
|
|
|
class RotatedSecretKeyTestCase(TestCase):
|
|
|
|
@staticmethod
|
|
def clear_cached_properties():
|
|
# we have to clear the cached properties of EncryptedFieldMixin so we have the right encryption keys
|
|
text_field = TestModel._meta.get_field('text')
|
|
if hasattr(text_field, 'keys'):
|
|
del text_field.keys
|
|
if hasattr(text_field, 'f'):
|
|
del text_field.f
|
|
|
|
@classmethod
|
|
@override_settings(SECRET_KEY="oldkey")
|
|
def setUpTestData(cls) -> None:
|
|
"""Create the initial record using the old key"""
|
|
cls.clear_cached_properties()
|
|
cls.original = TestModel.objects.create(text="Oh hi test reader")
|
|
cls.clear_cached_properties()
|
|
|
|
def tearDown(self):
|
|
self.clear_cached_properties()
|
|
|
|
@override_settings(SECRET_KEY="newkey", SECRET_KEY_FALLBACKS=["oldkey"])
|
|
def test_old_and_new_secret_keys(self) -> None:
|
|
|
|
plaintext = "Oh hi test reader"
|
|
model = TestModel()
|
|
model.text = plaintext
|
|
model.save()
|
|
|
|
fresh_model = TestModel.objects.get(id=model.id)
|
|
assert fresh_model.text == plaintext
|
|
|
|
old_record = TestModel.objects.get(id=self.original.id)
|
|
assert old_record.text == plaintext
|
|
|
|
@override_settings(SECRET_KEY="newkey")
|
|
def test_cannot_decrypt_old_record_with_new_key(self) -> None:
|
|
plaintext = "Oh hi test reader"
|
|
model = TestModel()
|
|
model.text = plaintext
|
|
model.save()
|
|
|
|
fresh_model = TestModel.objects.get(id=model.id)
|
|
assert fresh_model.text == plaintext
|
|
|
|
old_record = TestModel.objects.get(id=self.original.id)
|
|
# assert that old record text is still encrypted
|
|
assert old_record.text.endswith("=")
|
|
# assert that old record cannot be decrypted now
|
|
assert old_record.text != plaintext
|
|
|