Merge pull request #22 from frgmt/main

Fix to use ruff and fix some
This commit is contained in:
fragment 2025-01-06 11:38:43 +09:00 committed by GitHub
commit adc35e961d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 271 additions and 231 deletions

View file

@ -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

10
.gitignore vendored
View file

@ -1,6 +1,8 @@
*.pyc
dist/
build/
*.egg-info/
*.pyc
build/
dist/
.idea
.pypirc
.pypirc
.ruff_cache
.venv

50
.pre-commit-config.yaml Normal file
View file

@ -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]

View file

@ -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

View file

@ -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.
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -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: |

View file

@ -1 +1 @@
from .fields import *
from .fields import * # noqa: F403

View file

@ -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)

View file

@ -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

View file

@ -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):

View file

@ -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)

25
pyproject.toml Normal file
View file

@ -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

View file

@ -1,4 +1,4 @@
cffi==1.14.6
cryptography==35.0.0
pycparser==2.20
Django>=2.2
Django>=3.2

View file

@ -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",