Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 26 additions & 11 deletions .github/workflows/lint-and-test.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
name: Lint & Test

on: [push]
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read
Expand All @@ -10,6 +18,12 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Python 3.10
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand All @@ -27,21 +41,22 @@ jobs:
black --check .

test:
runs-on: ubuntu-latest
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.8", "3.9", "3.10", "3.11", "3.12"]
django_version: ["3.2", "4.0", "4.1", "4.2", "5.0"]
exclude:
# Ignore Django 4 on Python 3.7
- python_version: 3.8
django_version: 4.2.2
runs-on: ubuntu-latest
- python_version: "3.8"
django_version: "5.0"
- python_version: "3.9"
django_version: "5.0"

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python_version }}
uses: actions/setup-python@v3
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python_version }}

Expand All @@ -54,10 +69,10 @@ jobs:

- name: Run tests
run: |
coverage3 run --source='./encrypted_fields' manage.py test
coverage run --source="./encrypted_fields" manage.py test
coverage xml

- name: "Upload coverage to Codecov"
uses: codecov/codecov-action@v2
uses: codecov/codecov-action@v3
with:
fail_ci_if_error: true
12 changes: 0 additions & 12 deletions .travis.yml

This file was deleted.

3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[![Build Status](https://api.travis-ci.com/jazzband/django-fernet-encrypted-fields.png)](https://travis-ci.com/jazzband/django-fernet-encrypted-fields)
[![Build Status](https://github.com/jazzband/django-fernet-encrypted-fields/actions/workflows/lint-and-test.yml/badge.svg)](https://github.com/jazzband/django-fernet-encrypted-fields/actions/workflows/lint-and-test.yml)
[![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/)

Expand Down Expand Up @@ -64,3 +64,4 @@ Currently build in and unit-tested fields. They have the same APIs as their non-
| `4.0` |:heavy_check_mark:|
| `4.1` |:heavy_check_mark:|
| `4.2` |:heavy_check_mark:|
| `5.0` |:heavy_check_mark:|
101 changes: 36 additions & 65 deletions encrypted_fields/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
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
from django.utils.encoding import force_bytes, force_str

class EncryptedFieldMixin(object):

class EncryptedFieldMixin:
@cached_property
def keys(self):
keys = []
Expand All @@ -21,7 +21,7 @@ def keys(self):
else [settings.SALT_KEY]
)
for salt_key in salt_keys:
salt = bytes(salt_key, "utf-8")
salt = force_bytes(salt_key)
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
Expand All @@ -30,9 +30,7 @@ def keys(self):
backend=default_backend(),
)
keys.append(
base64.urlsafe_b64encode(
kdf.derive(settings.SECRET_KEY.encode("utf-8"))
)
base64.urlsafe_b64encode(kdf.derive(force_bytes(settings.SECRET_KEY)))
)
return keys

Expand All @@ -46,14 +44,14 @@ def get_internal_type(self):
"""
To treat everything as text
"""
return "TextField"
return getattr(self, "_internal_type", "TextField")

def get_prep_value(self, value):
value = super().get_prep_value(value)
if value:
if not isinstance(value, str):
value = str(value)
return self.f.encrypt(bytes(value, "utf-8")).decode("utf-8")
return force_str(self.f.encrypt(force_bytes(value)))
return None

def get_db_prep_value(self, value, connection, prepared=False):
Expand All @@ -64,20 +62,23 @@ def get_db_prep_value(self, value, connection, prepared=False):
def from_db_value(self, value, expression, connection):
return self.to_python(value)

def decrypt(self, value):
try:
value = force_str(self.f.decrypt(force_bytes(value)))
except (InvalidToken, UnicodeEncodeError):
pass
return value

def to_python(self, value):
if (
value is None
or not isinstance(value, str)
or hasattr(self, "_already_decrypted")
):
return value
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)

value = self.decrypt(value)
return super().to_python(value)

def clean(self, value, model_instance):
"""
Expand All @@ -89,6 +90,17 @@ def clean(self, value, model_instance):
del self._already_decrypted
return ret

@cached_property
def validators(self):
# Temporarily pretend to be whatever type of field we're masquerading
# as, for purposes of constructing validators (needed for
# IntegerField and subclasses).
self._internal_type = super().get_internal_type()
try:
return super().validators
finally:
del self._internal_type


class EncryptedCharField(EncryptedFieldMixin, models.CharField):
pass
Expand All @@ -103,40 +115,7 @@ class EncryptedDateTimeField(EncryptedFieldMixin, models.DateTimeField):


class EncryptedIntegerField(EncryptedFieldMixin, models.IntegerField):
@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_
pass


class EncryptedDateField(EncryptedFieldMixin, models.DateField):
Expand All @@ -160,39 +139,31 @@ def _encrypt_values(self, value):
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]
return [self._encrypt_values(data) for data in value]
else:
value = str(value)
return self.f.encrypt(bytes(value, "utf-8")).decode("utf-8")
return force_str(self.f.encrypt(force_bytes(value)))

def _decrypt_values(self, value):
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]
return [self._decrypt_values(data) for data in value]
else:
value = str(value)
return self.f.decrypt(bytes(value, "utf-8")).decode("utf-8")
return force_str(self.f.decrypt(force_bytes(value)))

def get_prep_value(self, value):
return json.dumps(self._encrypt_values(value=value), cls=self.encoder)

def get_internal_type(self):
return "JSONField"

def to_python(self, value):
if (
value is None
or not isinstance(value, str)
or hasattr(self, "_already_decrypted")
):
return value
def decrypt(self, value):
try:
value = self._decrypt_values(value=json.loads(value))
except InvalidToken:
pass
except UnicodeEncodeError:
except (InvalidToken, UnicodeEncodeError):
pass
return super(EncryptedFieldMixin, self).to_python(value)
return value
53 changes: 0 additions & 53 deletions lint-and-test.yml

This file was deleted.

2 changes: 1 addition & 1 deletion manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import sys

if __name__ == "__main__":
os.environ["DJANGO_SETTINGS_MODULE"] = "package_test.settings"
os.environ["DJANGO_SETTINGS_MODULE"] = "tests.settings"

from django.core.management import execute_from_command_line

Expand Down
6 changes: 2 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
cffi==1.14.6
cryptography==35.0.0
pycparser==2.20
Django>=2.2
cryptography>=35.0.0
Django>=3.2
3 changes: 1 addition & 2 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from __future__ import print_function
from setuptools import setup

setup(
name="django-fernet-encrypted-fields",
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="https://github.com/jazzband/django-fernet-encrypted-fields",
license="MIT",
author="fragment.co.jp",
author_email="[email protected]",
Expand Down
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions package_test/settings.py → tests/settings.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"ENGINE": "tests.sqlite3",
"NAME": ":memory:",
},
}

SECRET_KEY = "abc"
SALT_KEY = "xyz"

INSTALLED_APPS = ("encrypted_fields", "package_test")
INSTALLED_APPS = ("encrypted_fields", "tests")

MIDDLEWARE_CLASSES = []
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
1 change: 1 addition & 0 deletions tests/sqlite3/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Override to default django SQLite backend required for integer validation test."""
Loading