diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index 41d3553..d23c565 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -1,6 +1,6 @@ name: Lint & Test -on: [push] +on: [push, pull_request] permissions: contents: read @@ -13,14 +13,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 black + pip install ruff black - - name: Lint with flake8 + - name: Lint with Ruff run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + ruff check . - name: Lint with Black run: | @@ -29,35 +26,35 @@ jobs: test: strategy: matrix: - python_version: 3.8, 3.9, '3.10'] - django_version: [3.2, 4.0, 4.1.0, 4.2.2] + python_version: ["3.10", 3.11, 3.12] + django_version: [3.2, 4.0, 4.1.0, 4.2.2, 5.0, 5.1.4] exclude: - # Ignore Django 4 on Python 3.7 - - python_version: 3.8 - django_version: 4.2.2 + - python_version: 3.12 + django_version: 3.2 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python_version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python_version }} + - name: Set up Python ${{ matrix.python_version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python_version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -q Django==${{ matrix.django_version }} - pip install coverage - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -q Django==${{ matrix.django_version }} + pip install coverage pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Run tests - run: | - coverage3 run --source='./encrypted_fields' manage.py test - coverage xml + - name: Run tests + run: | + coverage3 run --source='./encrypted_fields' manage.py test + coverage xml - - name: "Upload coverage to Codecov" - uses: codecov/codecov-action@v2 - with: - fail_ci_if_error: true +# - name: "Upload coverage to Artifact" +# uses: actions/upload-artifact@v4 +# with: +# name: coverage +# path: coverage.xml diff --git a/.gitignore b/.gitignore index 21ed6a2..d229707 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ -*.pyc -dist/ -build/ *.egg-info/ +*.pyc +build/ +dist/ .idea -.pypirc \ No newline at end of file +.pypirc +.ruff_cache +.venv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..48dbd4f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,50 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks + +# exclude docs and static css +exclude: | + (?x)^( + package_test/.* + )$ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace # Trims trailing whitespace. + args: [--markdown-linebreak-ext=md] + - id: check-ast # Checks whether the files parse as valid python. + - id: check-case-conflict # Checks for files that would conflict in case-insensitive filesystems. + - id: check-json # Attempts to load all json files to verify syntax + - id: check-merge-conflict # Check for files that contain merge conflict strings + - id: check-xml # Attempts to load all xml files to verify syntax + - id: check-toml # Attempts to load all toml files to verify syntax + - id: check-yaml # Attempts to load all yaml files to verify syntax + args: [--unsafe] + - id: end-of-file-fixer # Makes sure files end in a newline and only a newline. + - id: check-symlinks # Checks for symlinks which do not point to anything + - id: debug-statements # Check for debugger imports and py37+ breakpoint() calls in python source + - id: check-added-large-files # Prevent giant files from being committed + + - repo: https://github.com/Lucas-C/pre-commit-hooks.git + rev: v1.5.5 + hooks: + - id: remove-crlf # Replace CRLF end-lines by LF ones before committing + - id: remove-tabs # Replace tabs by whitespaces before committing + + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.10.0 + hooks: + - id: python-use-type-annotations + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.1.0 + hooks: + - id: prettier # Autoformat yaml and markdown files + types_or: [yaml, markdown] + + - repo: https://github.com/asottile/pyupgrade + rev: v3.16.0 + hooks: + - id: pyupgrade + name: pyupgrade + args: [--py312-plus] diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index abb7406..0000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -language: python -python: - - "3.9" -env: - - DJANGO_VERSION=3.2 - - DJANGO_VERSION=4.0 - - DJANGO_VERSION=4.1 - - DJANGO_VERSION=4.2 -install: - - pip install -q Django==$DJANGO_VERSION - - pip install -q -r requirements.txt -script: python manage.py test \ No newline at end of file diff --git a/LICENCE.txt b/LICENCE.txt index 5b922ab..bf8f300 100644 --- a/LICENCE.txt +++ b/LICENCE.txt @@ -17,4 +17,4 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 24c0dc0..9c89443 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,36 @@ -[![Build Status](https://api.travis-ci.com/jazzband/django-fernet-encrypted-fields.png)](https://travis-ci.com/jazzband/django-fernet-encrypted-fields) [![Pypi Package](https://badge.fury.io/py/django-fernet-encrypted-fields.png)](http://badge.fury.io/py/django-fernet-encrypted-fields) [![Jazzband](https://jazzband.co/static/img/badge.svg)](https://jazzband.co/) - ### Django Fernet Encrypted Fields This package was created as a successor to [django-encrypted-fields](https://github.com/defrex/django-encrypted-fields). #### Getting Started + ```shell $ pip install django-fernet-encrypted-fields ``` + In your `settings.py`, set random SALT_KEY + ```python SALT_KEY = '0123456789abcdefghijklmnopqrstuvwxyz' ``` Then, in `models.py` + ```python from encrypted_fields.fields import EncryptedTextField class MyModel(models.Model): text_field = EncryptedTextField() ``` + Use your model as normal and your data will be encrypted in the database. #### Rotating SALT keys -You can rotate salt keys by turning the ```SALT_KEY``` settings.py entry into a list. The first key will be used to encrypt all new data, and decryption of existing values will be attempted with all given keys in order. This is useful for key rotation: place a new key at the head of the list for use with all new or changed data, but existing values encrypted with old keys will still be accessible + +You can rotate salt keys by turning the `SALT_KEY` settings.py entry into a list. The first key will be used to encrypt all new data, and decryption of existing values will be attempted with all given keys in order. This is useful for key rotation: place a new key at the head of the list for use with all new or changed data, but existing values encrypted with old keys will still be accessible ```python SALT_KEY = [ @@ -42,7 +46,6 @@ for obj in MuModel.objects.all(): obj.save() ``` - #### Available Fields Currently build in and unit-tested fields. They have the same APIs as their non-encrypted counterparts. @@ -58,9 +61,11 @@ Currently build in and unit-tested fields. They have the same APIs as their non- ### Compatible Django Version -| Compatible Django Version |Specifically tested| -|---------------------------|---| -| `3.2` |:heavy_check_mark:| -| `4.0` |:heavy_check_mark:| -| `4.1` |:heavy_check_mark:| -| `4.2` |:heavy_check_mark:| +| Compatible Django Version | Specifically tested | +| ------------------------- | ------------------- | +| `3.2` | :heavy_check_mark: | +| `4.0` | :heavy_check_mark: | +| `4.1` | :heavy_check_mark: | +| `4.2` | :heavy_check_mark: | +| `5.0` | :heavy_check_mark: | +| `5.1` | :heavy_check_mark: | diff --git a/encrypted_fields/__init__.py b/encrypted_fields/__init__.py index 7746f2c..0a9ff24 100644 --- a/encrypted_fields/__init__.py +++ b/encrypted_fields/__init__.py @@ -1 +1 @@ -from .fields import * +from .fields import * # noqa: F403 diff --git a/encrypted_fields/fields.py b/encrypted_fields/fields.py index 03e5c45..82bc216 100644 --- a/encrypted_fields/fields.py +++ b/encrypted_fields/fields.py @@ -1,19 +1,27 @@ +from __future__ import annotations + import base64 import json +from typing import Any -from cryptography.fernet import Fernet, MultiFernet, InvalidToken +from cryptography.fernet import Fernet, InvalidToken, 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.core import validators +from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models +from django.db.backends.base.base import BaseDatabaseWrapper from django.db.backends.base.operations import BaseDatabaseOperations +from django.db.models.expressions import Expression from django.utils.functional import cached_property -class EncryptedFieldMixin(object): +_TypeAny = Any + + +class EncryptedFieldMixin: @cached_property - def keys(self): + def keys(self) -> list[bytes]: keys = [] salt_keys = ( settings.SALT_KEY @@ -37,18 +45,18 @@ class EncryptedFieldMixin(object): return keys @cached_property - def f(self): + def f(self) -> Fernet | MultiFernet: if len(self.keys) == 1: return Fernet(self.keys[0]) return MultiFernet([Fernet(k) for k in self.keys]) - def get_internal_type(self): + def get_internal_type(self) -> str: """ To treat everything as text """ return "TextField" - def get_prep_value(self, value): + def get_prep_value(self, value: _TypeAny) -> _TypeAny: value = super().get_prep_value(value) if value: if not isinstance(value, str): @@ -56,15 +64,25 @@ class EncryptedFieldMixin(object): return self.f.encrypt(bytes(value, "utf-8")).decode("utf-8") return None - def get_db_prep_value(self, value, connection, prepared=False): + def get_db_prep_value( + self, + value: _TypeAny, + connection: BaseDatabaseWrapper, # noqa: ARG002 + prepared: bool = False, # noqa: FBT001, FBT002 + ) -> _TypeAny: if not prepared: value = self.get_prep_value(value) return value - def from_db_value(self, value, expression, connection): + def from_db_value( + self, + value: _TypeAny, + expression: Expression, # noqa: ARG002 + connection: BaseDatabaseWrapper, # noqa: ARG002 + ) -> _TypeAny: return self.to_python(value) - def to_python(self, value): + def to_python(self, value: _TypeAny) -> _TypeAny: if ( value is None or not isinstance(value, str) @@ -77,12 +95,12 @@ class EncryptedFieldMixin(object): pass except UnicodeEncodeError: pass - return super(EncryptedFieldMixin, self).to_python(value) + return super().to_python(value) - def clean(self, value, model_instance): + def clean(self, value: _TypeAny, model_instance: models.Field) -> _TypeAny: """ - Create and assign a semaphore so that to_python method will not try to decrypt an already decrypted value - during cleaning of a form + Create and assign a semaphore so that to_python method will not try + to decrypt an already decrypted value during cleaning of a form """ self._already_decrypted = True ret = super().clean(value, model_instance) @@ -104,15 +122,17 @@ class EncryptedDateTimeField(EncryptedFieldMixin, models.DateTimeField): class EncryptedIntegerField(EncryptedFieldMixin, models.IntegerField): @cached_property - def validators(self): + def validators(self) -> list[MinValueValidator | MaxValueValidator]: # 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] + min_value, max_value = BaseDatabaseOperations.integer_field_ranges[ + internal_type + ] if min_value is not None and not any( ( - isinstance(validator, validators.MinValueValidator) + isinstance(validator, MinValueValidator) and ( validator.limit_value() if callable(validator.limit_value) @@ -122,10 +142,10 @@ class EncryptedIntegerField(EncryptedFieldMixin, models.IntegerField): ) for validator in validators_ ): - validators_.append(validators.MinValueValidator(min_value)) + validators_.append(MinValueValidator(min_value)) if max_value is not None and not any( ( - isinstance(validator, validators.MaxValueValidator) + isinstance(validator, MaxValueValidator) and ( validator.limit_value() if callable(validator.limit_value) @@ -135,7 +155,7 @@ class EncryptedIntegerField(EncryptedFieldMixin, models.IntegerField): ) for validator in validators_ ): - validators_.append(validators.MaxValueValidator(max_value)) + validators_.append(MaxValueValidator(max_value)) return validators_ @@ -156,33 +176,31 @@ class EncryptedBooleanField(EncryptedFieldMixin, models.BooleanField): class EncryptedJSONField(EncryptedFieldMixin, models.JSONField): - def _encrypt_values(self, value): + def _encrypt_values(self, value: _TypeAny) -> _TypeAny: if isinstance(value, dict): return {key: self._encrypt_values(data) for key, data in value.items()} - elif isinstance(value, list): - return [self._encrypt_values(data) for data in value] - else: - value = str(value) + if isinstance(value, list): + return [self._encrypt_values(data) for data in value] + value = str(value) return self.f.encrypt(bytes(value, "utf-8")).decode("utf-8") - def _decrypt_values(self, value): + def _decrypt_values(self, value: _TypeAny) -> _TypeAny: if value is None: return value if isinstance(value, dict): return {key: self._decrypt_values(data) for key, data in value.items()} - elif isinstance(value, list): - return [self._decrypt_values(data) for data in value] - else: - value = str(value) + if isinstance(value, list): + return [self._decrypt_values(data) for data in value] + value = str(value) return self.f.decrypt(bytes(value, "utf-8")).decode("utf-8") - def get_prep_value(self, value): + def get_prep_value(self, value: _TypeAny) -> str: return json.dumps(self._encrypt_values(value=value), cls=self.encoder) - def get_internal_type(self): + def get_internal_type(self) -> str: return "JSONField" - def to_python(self, value): + def to_python(self, value: _TypeAny) -> _TypeAny: if ( value is None or not isinstance(value, str) @@ -195,4 +213,4 @@ class EncryptedJSONField(EncryptedFieldMixin, models.JSONField): pass except UnicodeEncodeError: pass - return super(EncryptedFieldMixin, self).to_python(value) + return super().to_python(value) diff --git a/lint-and-test.yml b/lint-and-test.yml deleted file mode 100644 index e3123ce..0000000 --- a/lint-and-test.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Lint & Test - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -permissions: - contents: read - -jobs: - build: - strategy: - matrix: - django_version: [2.2, 3.0, 3.1, 3.2, 4.0a1] - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - name: Set up Python 3.10 - uses: actions/setup-python@v3 - with: - python-version: "3.10" - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -q Django==${{ matrix.django_version }} - pip install flake8 coverage black - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - - name: Lint with Black - run: | - black --check . - - - name: Run tests - run: | - coverage3 run --source='./encrypted_fields' manage.py test - coverage xml - - - name: "Upload coverage to Codecov" - uses: codecov/codecov-action@v2 - with: - fail_ci_if_error: true diff --git a/package_test/models.py b/package_test/models.py index 115e7c9..9d56770 100644 --- a/package_test/models.py +++ b/package_test/models.py @@ -1,4 +1,16 @@ -from encrypted_fields.fields import * +from django.db import models + +from encrypted_fields.fields import ( + EncryptedBooleanField, + EncryptedCharField, + EncryptedDateField, + EncryptedDateTimeField, + EncryptedEmailField, + EncryptedFloatField, + EncryptedIntegerField, + EncryptedJSONField, + EncryptedTextField, +) class TestModel(models.Model): diff --git a/package_test/tests.py b/package_test/tests.py index 9d2c9b1..d62e9b0 100644 --- a/package_test/tests.py +++ b/package_test/tests.py @@ -1,25 +1,24 @@ 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 django.core.exceptions import ValidationError from .models import TestModel class FieldTest(TestCase): - def get_db_value(self, field, model_id): + def get_db_value(self, field: str, model_id: int) -> None: cursor = connection.cursor() cursor.execute( - "select {0} " - "from package_test_testmodel " - "where id = {1};".format(field, model_id) + f"select {field} from package_test_testmodel where id = {model_id};" ) return cursor.fetchone()[0] - def test_char_field_encrypted(self): + def test_char_field_encrypted(self) -> None: plaintext = "Oh hi, test reader!" model = TestModel() @@ -29,13 +28,13 @@ class FieldTest(TestCase): ciphertext = self.get_db_value("char", model.id) - self.assertNotEqual(plaintext, ciphertext) - self.assertTrue("test" not in ciphertext) + assert plaintext != ciphertext + assert "test" not in ciphertext fresh_model = TestModel.objects.get(id=model.id) - self.assertEqual(fresh_model.char, plaintext) + assert fresh_model.char == plaintext - def test_text_field_encrypted(self): + def test_text_field_encrypted(self) -> None: plaintext = "Oh hi, test reader!" * 10 model = TestModel() @@ -45,13 +44,13 @@ class FieldTest(TestCase): ciphertext = self.get_db_value("text", model.id) - self.assertNotEqual(plaintext, ciphertext) - self.assertTrue("test" not in ciphertext) + assert plaintext != ciphertext + assert "test" not in ciphertext fresh_model = TestModel.objects.get(id=model.id) - self.assertEqual(fresh_model.text, plaintext) + assert fresh_model.text == plaintext - def test_datetime_field_encrypted(self): + def test_datetime_field_encrypted(self) -> None: plaintext = timezone.now() model = TestModel() @@ -62,19 +61,19 @@ class FieldTest(TestCase): ciphertext = self.get_db_value("datetime", model.id) # Django's normal date serialization format - self.assertTrue(re.search("^\d\d\d\d-\d\d-\d\d", ciphertext) is None) + assert re.search(r"^\d\d\d\d-\d\d-\d\d", ciphertext) is None fresh_model = TestModel.objects.get(id=model.id) - self.assertEqual(fresh_model.datetime, plaintext) + assert fresh_model.datetime == plaintext plaintext = "text" + model.datetime = plaintext + model.full_clean() - with self.assertRaises(ValidationError): - model.datetime = plaintext - model.full_clean() + with pytest.raises(ValidationError): model.save() - def test_integer_field_encrypted(self): + def test_integer_field_encrypted(self) -> None: plaintext = 42 model = TestModel() @@ -84,28 +83,26 @@ class FieldTest(TestCase): ciphertext = self.get_db_value("integer", model.id) - self.assertNotEqual(plaintext, ciphertext) - self.assertNotEqual(plaintext, str(ciphertext)) + assert plaintext != ciphertext + assert plaintext != str(ciphertext) fresh_model = TestModel.objects.get(id=model.id) - self.assertEqual(fresh_model.integer, plaintext) + assert fresh_model.integer == plaintext # "IntegerField": (-2147483648, 2147483647) plaintext = 2147483648 + model.integer = plaintext - with self.assertRaises(ValidationError): - model.integer = plaintext + with pytest.raises(ValidationError): model.full_clean() - model.save() plaintext = "text" + model.integer = plaintext - with self.assertRaises(TypeError): - model.integer = plaintext + with pytest.raises(TypeError): model.full_clean() - model.save() - def test_date_field_encrypted(self): + def test_date_field_encrypted(self) -> None: plaintext = timezone.now().date() model = TestModel() @@ -116,17 +113,17 @@ class FieldTest(TestCase): ciphertext = self.get_db_value("date", model.id) fresh_model = TestModel.objects.get(id=model.id) - self.assertNotEqual(ciphertext, plaintext.isoformat()) - self.assertEqual(fresh_model.date, plaintext) + assert ciphertext != plaintext.isoformat() + assert fresh_model.date == plaintext plaintext = "text" + model.date = plaintext + model.full_clean() - with self.assertRaises(ValidationError): - model.date = plaintext - model.full_clean() + with pytest.raises(ValidationError): model.save() - def test_float_field_encrypted(self): + def test_float_field_encrypted(self) -> None: plaintext = 42.44 model = TestModel() @@ -136,20 +133,20 @@ class FieldTest(TestCase): ciphertext = self.get_db_value("floating", model.id) - self.assertNotEqual(plaintext, ciphertext) - self.assertNotEqual(plaintext, str(ciphertext)) + assert plaintext != ciphertext + assert plaintext != str(ciphertext) fresh_model = TestModel.objects.get(id=model.id) - self.assertEqual(fresh_model.floating, plaintext) + assert fresh_model.floating == plaintext plaintext = "text" + model.floating = plaintext + model.full_clean() - with self.assertRaises(ValueError): - model.floating = plaintext - model.full_clean() + with pytest.raises(ValueError): model.save() - def test_email_field_encrypted(self): + def test_email_field_encrypted(self) -> None: plaintext = "test@gmail.com" model = TestModel() @@ -159,20 +156,19 @@ class FieldTest(TestCase): ciphertext = self.get_db_value("email", model.id) - self.assertNotEqual(plaintext, ciphertext) - self.assertTrue("aron" not in ciphertext) + assert plaintext != ciphertext + assert "aron" not in ciphertext fresh_model = TestModel.objects.get(id=model.id) - self.assertEqual(fresh_model.email, plaintext) + assert fresh_model.email == plaintext plaintext = "text" + model.email = plaintext - with self.assertRaises(ValidationError): - model.email = plaintext + with pytest.raises(ValidationError): model.full_clean() - model.save() - def test_boolean_field_encrypted(self): + def test_boolean_field_encrypted(self) -> None: plaintext = True model = TestModel() @@ -182,26 +178,30 @@ class FieldTest(TestCase): ciphertext = self.get_db_value("boolean", model.id) - self.assertNotEqual(plaintext, ciphertext) - self.assertNotEqual(True, ciphertext) - self.assertNotEqual("True", ciphertext) - self.assertNotEqual("true", ciphertext) - self.assertNotEqual("1", ciphertext) - self.assertNotEqual(1, ciphertext) - self.assertTrue(not isinstance(ciphertext, bool)) + 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) - self.assertEqual(fresh_model.boolean, plaintext) + assert fresh_model.boolean == plaintext plaintext = "text" + model.boolean = plaintext + model.full_clean() - with self.assertRaises(ValidationError): - model.boolean = plaintext - model.full_clean() + with pytest.raises(ValidationError): model.save() - def test_json_field_encrypted(self): - dict_values = {"key": "value", "list": ["nested", {"key": "val"}], "nested": {"child": "sibling"}} + def test_json_field_encrypted(self) -> None: + dict_values = { + "key": "value", + "list": ["nested", {"key": "val"}], + "nested": {"child": "sibling"}, + } model = TestModel() model.json = dict_values @@ -210,15 +210,14 @@ class FieldTest(TestCase): ciphertext = json.loads(self.get_db_value("json", model.id)) - - self.assertNotEqual(dict_values, ciphertext) + assert dict_values != ciphertext fresh_model = TestModel.objects.get(id=model.id) - self.assertEqual(fresh_model.json, dict_values) + assert fresh_model.json == dict_values - def test_json_field_retains_keys(self): + 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() @@ -226,18 +225,18 @@ class FieldTest(TestCase): ciphertext = json.loads(self.get_db_value("json", model.id)) - self.assertEqual(plain_value.keys(), ciphertext.keys()) + assert plain_value.keys() == ciphertext.keys() class RotatedSaltTestCase(TestCase): @classmethod @override_settings(SALT_KEY=["abcdefghijklmnopqrstuvwxyz0123456789"]) - def setUpTestData(cls): + 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): + 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() @@ -246,15 +245,13 @@ class RotatedSaltTestCase(TestCase): ciphertext = FieldTest.get_db_value(self, "text", model.id) - self.assertNotEqual(plaintext, ciphertext) - self.assertTrue("test" not in ciphertext) + assert plaintext != ciphertext + assert "test" not in ciphertext fresh_model = TestModel.objects.get(id=model.id) - self.assertEqual(fresh_model.text, plaintext) + assert fresh_model.text == plaintext old_record = TestModel.objects.get(id=self.original.id) - self.assertEqual(fresh_model.text, old_record.text) + assert fresh_model.text == old_record.text - self.assertNotEqual( - ciphertext, FieldTest.get_db_value(self, "text", self.original.pk) - ) + assert ciphertext != FieldTest.get_db_value(self, "text", self.original.pk) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8a2f802 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +################## +# ruff +################## +[tool.ruff] +fix = true +lint.fixable = ["ALL"] +lint.ignore = ["A003", "COM812", "D", "DJ008", "ERA001", "ISC001", "PLC2401", "PLC2403", "PT011", "RUF001", "S101", "S105", "S608", "SIM103", "TC001", "TC002", "TC003", "UP040"] +lint.select = ["ALL"] +lint.unfixable = ["ERA001", "F401"] +include = ["encrypted_fields/*.py", "package_test/*.py"] +target-version = "py312" + +################## +# mypy +################## +[tool.mypy] +mypy_path = "$MYPY_CONFIG_FILE_DIR" +packages = ["encrypted_fields"] +python_version = "3.12" + +strict = true +warn_unreachable = true +warn_no_return = true +ignore_missing_imports = true +disallow_untyped_decorators = false diff --git a/requirements.txt b/requirements.txt index 7cf0930..de8bfbc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ cffi==1.14.6 cryptography==35.0.0 pycparser==2.20 -Django>=2.2 +Django>=3.2 diff --git a/setup.py b/setup.py index f1f2ee0..74190ba 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ -from __future__ import print_function from setuptools import setup setup( @@ -6,12 +5,12 @@ setup( description=("This is inspired by django-encrypted-fields."), long_description=open("README.md").read(), long_description_content_type="text/markdown", - url="http://github.com/frgmt/django-fernet-encrypted-fields/", + url="http://github.com/jazzband/django-fernet-encrypted-fields/", license="MIT", - author="fragment.co.jp", - author_email="info@fragment.co.jp", + author="jazzband", + author_email="n.anahara@fragment.co.jp", packages=["encrypted_fields"], - version="0.1.3", + version="0.2.0", install_requires=[ "Django>=3.2", "cryptography>=35.0.0",