mirror of
https://github.com/jazzband/django-fernet-encrypted-fields.git
synced 2026-03-16 22:40:27 +00:00
first commit
This commit is contained in:
commit
6e3ea22405
13 changed files with 367 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
*.pyc
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.egg-info/
|
||||||
|
.idea
|
||||||
|
.pypirc
|
||||||
13
.travis.yml
Normal file
13
.travis.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
language: python
|
||||||
|
python:
|
||||||
|
- "3.9"
|
||||||
|
env:
|
||||||
|
- DJANGO_VERSION=2.2
|
||||||
|
- DJANGO_VERSION=3.0
|
||||||
|
- DJANGO_VERSION=3.1
|
||||||
|
- DJANGO_VERSION=3.2
|
||||||
|
- DJANGO_VERSION=4.0a1
|
||||||
|
install:
|
||||||
|
- pip install -q Django==$DJANGO_VERSION
|
||||||
|
- pip install -q -r requirements.txt
|
||||||
|
script: python manage.py test
|
||||||
20
LICENCE.txt
Normal file
20
LICENCE.txt
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2021 fragment.co.jp
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
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.
|
||||||
46
README.md
Normal file
46
README.md
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
[](https://travis-ci.com/frgmt/django-fernet-encrypted-fields)
|
||||||
|
[](http://badge.fury.io/py/django-fernet-encrypted-fields)
|
||||||
|
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
#### Available Fields
|
||||||
|
|
||||||
|
Currently build in and unit-tested fields. They have the same APIs as their non-encrypted counterparts.
|
||||||
|
|
||||||
|
- `EncryptedCharField`
|
||||||
|
- `EncryptedTextField`
|
||||||
|
- `EncryptedDateTimeField`
|
||||||
|
- `EncryptedIntegerField`
|
||||||
|
- `EncryptedFloatField`
|
||||||
|
- `EncryptedEmailField`
|
||||||
|
- `EncryptedBooleanField`
|
||||||
|
|
||||||
|
### Compatible Django Version
|
||||||
|
|
||||||
|
|Compatible Django Version|Specifically tested|
|
||||||
|
|---|---|
|
||||||
|
|`2.2`|:heavy_check_mark:|
|
||||||
|
|`3.0`|:heavy_check_mark:|
|
||||||
|
|`3.1`|:heavy_check_mark:|
|
||||||
|
|`3.2`|:heavy_check_mark:|
|
||||||
|
|`4.0`|:heavy_check_mark:|
|
||||||
1
encrypted_fields/__init__.py
Normal file
1
encrypted_fields/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from .fields import *
|
||||||
78
encrypted_fields/fields.py
Normal file
78
encrypted_fields/fields.py
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import base64
|
||||||
|
from django.conf import settings
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class EncryptedFieldMixin(object):
|
||||||
|
salt = bytes(settings.SALT_KEY, 'utf-8')
|
||||||
|
kdf = PBKDF2HMAC(algorithm=hashes.SHA256(),
|
||||||
|
length=32,
|
||||||
|
salt=salt,
|
||||||
|
iterations=100000,
|
||||||
|
backend=default_backend())
|
||||||
|
|
||||||
|
key = base64.urlsafe_b64encode(kdf.derive(settings.SECRET_KEY.encode('utf-8')))
|
||||||
|
f = Fernet(key)
|
||||||
|
|
||||||
|
def get_internal_type(self):
|
||||||
|
"""
|
||||||
|
To treat everything as text
|
||||||
|
"""
|
||||||
|
return 'TextField'
|
||||||
|
|
||||||
|
def get_prep_value(self, value):
|
||||||
|
if value:
|
||||||
|
if not isinstance(value, str):
|
||||||
|
value = str(value)
|
||||||
|
return self.f.encrypt(bytes(value, 'utf-8')).decode('utf-8')
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_db_prep_value(self, value, connection, prepared=False):
|
||||||
|
if not prepared:
|
||||||
|
value = self.get_prep_value(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def from_db_value(self, value, expression, connection):
|
||||||
|
return self.to_python(value)
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
if value is None or not isinstance(value, str):
|
||||||
|
return value
|
||||||
|
value = self.f.decrypt(bytes(value, 'utf-8')).decode('utf-8')
|
||||||
|
return super(EncryptedFieldMixin, self).to_python(value)
|
||||||
|
|
||||||
|
|
||||||
|
class EncryptedCharField(EncryptedFieldMixin, models.CharField):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EncryptedTextField(EncryptedFieldMixin, models.TextField):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EncryptedDateTimeField(EncryptedFieldMixin, models.DateTimeField):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EncryptedIntegerField(EncryptedFieldMixin, models.IntegerField):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EncryptedDateField(EncryptedFieldMixin, models.DateField):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EncryptedFloatField(EncryptedFieldMixin, models.FloatField):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EncryptedEmailField(EncryptedFieldMixin, models.EmailField):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EncryptedBooleanField(EncryptedFieldMixin, models.BooleanField):
|
||||||
|
pass
|
||||||
9
manage.py
Normal file
9
manage.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
os.environ['DJANGO_SETTINGS_MODULE'] = 'package_test.settings'
|
||||||
|
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
0
package_test/__init__.py
Normal file
0
package_test/__init__.py
Normal file
12
package_test/models.py
Normal file
12
package_test/models.py
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
from encrypted_fields.fields import *
|
||||||
|
|
||||||
|
|
||||||
|
class TestModel(models.Model):
|
||||||
|
char = EncryptedCharField(max_length=255, null=True, blank=True)
|
||||||
|
text = EncryptedTextField(null=True, blank=True)
|
||||||
|
datetime = EncryptedDateTimeField(null=True, blank=True)
|
||||||
|
integer = EncryptedIntegerField(null=True, blank=True)
|
||||||
|
date = EncryptedDateField(null=True, blank=True)
|
||||||
|
floating = EncryptedFloatField(null=True, blank=True)
|
||||||
|
email = EncryptedEmailField(null=True, blank=True)
|
||||||
|
boolean = EncryptedBooleanField(default=False, null=True)
|
||||||
18
package_test/settings.py
Normal file
18
package_test/settings.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': ':memory:',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
SECRET_KEY = 'abc'
|
||||||
|
SALT_KEY = 'xyz'
|
||||||
|
|
||||||
|
INSTALLED_APPS = (
|
||||||
|
'encrypted_fields',
|
||||||
|
'package_test'
|
||||||
|
)
|
||||||
|
|
||||||
|
MIDDLEWARE_CLASSES = []
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||||
|
|
||||||
141
package_test/tests.py
Normal file
141
package_test/tests.py
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
from django.db import connection
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from .models import TestModel
|
||||||
|
|
||||||
|
|
||||||
|
class FieldTest(TestCase):
|
||||||
|
def get_db_value(self, field, model_id):
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
'select {0} '
|
||||||
|
'from package_test_testmodel '
|
||||||
|
'where id = {1};'.format(field, model_id)
|
||||||
|
)
|
||||||
|
return cursor.fetchone()[0]
|
||||||
|
|
||||||
|
def test_char_field_encrypted(self):
|
||||||
|
plaintext = 'Oh hi, test reader!'
|
||||||
|
|
||||||
|
model = TestModel()
|
||||||
|
model.char = plaintext
|
||||||
|
model.save()
|
||||||
|
|
||||||
|
ciphertext = self.get_db_value('char', model.id)
|
||||||
|
|
||||||
|
self.assertNotEqual(plaintext, ciphertext)
|
||||||
|
self.assertTrue('test' not in ciphertext)
|
||||||
|
|
||||||
|
fresh_model = TestModel.objects.get(id=model.id)
|
||||||
|
self.assertEqual(fresh_model.char, plaintext)
|
||||||
|
|
||||||
|
def test_text_field_encrypted(self):
|
||||||
|
plaintext = 'Oh hi, test reader!' * 10
|
||||||
|
|
||||||
|
model = TestModel()
|
||||||
|
model.text = plaintext
|
||||||
|
model.save()
|
||||||
|
|
||||||
|
ciphertext = self.get_db_value('text', model.id)
|
||||||
|
|
||||||
|
self.assertNotEqual(plaintext, ciphertext)
|
||||||
|
self.assertTrue('test' not in ciphertext)
|
||||||
|
|
||||||
|
fresh_model = TestModel.objects.get(id=model.id)
|
||||||
|
self.assertEqual(fresh_model.text, plaintext)
|
||||||
|
|
||||||
|
def test_datetime_field_encrypted(self):
|
||||||
|
plaintext = timezone.now()
|
||||||
|
|
||||||
|
model = TestModel()
|
||||||
|
model.datetime = plaintext
|
||||||
|
model.save()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
fresh_model = TestModel.objects.get(id=model.id)
|
||||||
|
self.assertEqual(fresh_model.datetime, plaintext)
|
||||||
|
|
||||||
|
def test_integer_field_encrypted(self):
|
||||||
|
plaintext = 42
|
||||||
|
|
||||||
|
model = TestModel()
|
||||||
|
model.integer = plaintext
|
||||||
|
model.save()
|
||||||
|
|
||||||
|
ciphertext = self.get_db_value('integer', model.id)
|
||||||
|
|
||||||
|
self.assertNotEqual(plaintext, ciphertext)
|
||||||
|
self.assertNotEqual(plaintext, str(ciphertext))
|
||||||
|
|
||||||
|
fresh_model = TestModel.objects.get(id=model.id)
|
||||||
|
self.assertEqual(fresh_model.integer, plaintext)
|
||||||
|
|
||||||
|
def test_date_field_encrypted(self):
|
||||||
|
plaintext = timezone.now().date()
|
||||||
|
|
||||||
|
model = TestModel()
|
||||||
|
model.date = plaintext
|
||||||
|
model.save()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def test_float_field_encrypted(self):
|
||||||
|
plaintext = 42.44
|
||||||
|
|
||||||
|
model = TestModel()
|
||||||
|
model.floating = plaintext
|
||||||
|
model.save()
|
||||||
|
|
||||||
|
ciphertext = self.get_db_value('floating', model.id)
|
||||||
|
|
||||||
|
self.assertNotEqual(plaintext, ciphertext)
|
||||||
|
self.assertNotEqual(plaintext, str(ciphertext))
|
||||||
|
|
||||||
|
fresh_model = TestModel.objects.get(id=model.id)
|
||||||
|
self.assertEqual(fresh_model.floating, plaintext)
|
||||||
|
|
||||||
|
def test_email_field_encrypted(self):
|
||||||
|
plaintext = 'test@gmail.com'
|
||||||
|
|
||||||
|
model = TestModel()
|
||||||
|
model.email = plaintext
|
||||||
|
model.save()
|
||||||
|
|
||||||
|
ciphertext = self.get_db_value('email', model.id)
|
||||||
|
|
||||||
|
self.assertNotEqual(plaintext, ciphertext)
|
||||||
|
self.assertTrue('aron' not in ciphertext)
|
||||||
|
|
||||||
|
fresh_model = TestModel.objects.get(id=model.id)
|
||||||
|
self.assertEqual(fresh_model.email, plaintext)
|
||||||
|
|
||||||
|
def test_boolean_field_encrypted(self):
|
||||||
|
plaintext = True
|
||||||
|
|
||||||
|
model = TestModel()
|
||||||
|
model.boolean = plaintext
|
||||||
|
model.save()
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
fresh_model = TestModel.objects.get(id=model.id)
|
||||||
|
self.assertEqual(fresh_model.boolean, plaintext)
|
||||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
cffi==1.14.6
|
||||||
|
cryptography==35.0.0
|
||||||
|
pycparser==2.20
|
||||||
|
Django>=2.2
|
||||||
19
setup.py
Normal file
19
setup.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
from __future__ import print_function
|
||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='django-fernet-encrypted-fields',
|
||||||
|
description=(
|
||||||
|
'This is inspired by django-encrypted-fields.'
|
||||||
|
),
|
||||||
|
url='http://github.com/frgmt/django-fernet-encrypted-fields/',
|
||||||
|
license='MIT',
|
||||||
|
author='fragment.co.jp',
|
||||||
|
author_email='info@fragment.co.jp',
|
||||||
|
packages=['encrypted_fields'],
|
||||||
|
version='0.0.1',
|
||||||
|
install_requires=[
|
||||||
|
'Django>=2.2',
|
||||||
|
'cryptography>=35.0.0',
|
||||||
|
],
|
||||||
|
)
|
||||||
Loading…
Reference in a new issue