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