├── testapp ├── __init__.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── admin.py ├── urls.py ├── models.py └── tests.py ├── cryptographic_fields ├── settings.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── generate_encryption_key.py ├── __init__.py ├── tests.py └── fields.py ├── MANIFEST.in ├── circle.yml ├── .gitignore ├── requirements.txt ├── manage.py ├── settings_circleci.py ├── settings_test.py ├── LICENSE ├── setup.py └── README.rst /testapp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cryptographic_fields/settings.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testapp/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cryptographic_fields/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cryptographic_fields/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | -------------------------------------------------------------------------------- /cryptographic_fields/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.5.1' 2 | -------------------------------------------------------------------------------- /testapp/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /testapp/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | 4 | urlpatterns = [] 5 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | environment: 3 | DJANGO_SETTINGS_MODULE: "settings_circleci" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pot 3 | *.pyc 4 | *.sqlite 5 | .DS_Store 6 | *~ 7 | *.sqlite3 8 | .idea 9 | dist 10 | *.egg-info 11 | build 12 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | setuptools 2 | coverage 3 | cryptography 4 | Django>=1.9.4 5 | django_coverage 6 | flake8 7 | ipython 8 | mock 9 | pep8 10 | psycopg2 11 | pyflakes 12 | pytz 13 | yolk 14 | freezegun 15 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings_test") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /cryptographic_fields/management/commands/generate_encryption_key.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | import cryptography.fernet 4 | 5 | 6 | class Command(BaseCommand): 7 | help = 'Generates a new Fernet encryption key' 8 | 9 | def handle(self, *args, **options): 10 | key = cryptography.fernet.Fernet.generate_key() 11 | self.stdout.write(key) 12 | -------------------------------------------------------------------------------- /settings_circleci.py: -------------------------------------------------------------------------------- 1 | INTERNAL_IPS = ('127.0.0.1', ) 2 | ALLOWED_HOSTS = ['localhost', '127.0.0.1', ] 3 | APPEND_SLASH = True 4 | TIME_ZONE = 'UTC' 5 | USE_TZ = True 6 | ROOT_URLCONF = 'testapp.urls' 7 | SECRET_KEY = 'abc' 8 | 9 | INSTALLED_APPS = ( 10 | 'django.contrib.auth', 11 | 'django.contrib.contenttypes', 12 | 'django.contrib.admin', 13 | 'django_coverage', 14 | 'cryptographic_fields', 15 | 'testapp', 16 | ) 17 | 18 | FIELD_ENCRYPTION_KEY = '6-QgONW6TUl5rt4Xq8u-wBwPcb15sIYS2CN6d69zueM=' 19 | 20 | DATABASES = { 21 | 'default': { 22 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 23 | 'USERNAME': 'ubuntu', 24 | 'HOST': '127.0.0.1', 25 | 'PORT': 5432, 26 | 'NAME': 'circle_test', 27 | } 28 | } 29 | 30 | MIDDLEWARE_CLASSES = ( 31 | 'django.middleware.common.CommonMiddleware', 32 | 'django.middleware.csrf.CsrfViewMiddleware', 33 | ) 34 | -------------------------------------------------------------------------------- /settings_test.py: -------------------------------------------------------------------------------- 1 | INTERNAL_IPS = ('127.0.0.1', ) 2 | ALLOWED_HOSTS = ['localhost', '127.0.0.1', ] 3 | APPEND_SLASH = True 4 | TIME_ZONE = 'UTC' 5 | USE_TZ = True 6 | ROOT_URLCONF = 'testapp.urls' 7 | SECRET_KEY = 'abc' 8 | 9 | INSTALLED_APPS = ( 10 | 'django.contrib.auth', 11 | 'django.contrib.contenttypes', 12 | 'django.contrib.admin', 13 | 'django_coverage', 14 | 'cryptographic_fields', 15 | 'testapp', 16 | ) 17 | 18 | FIELD_ENCRYPTION_KEY = '6-QgONW6TUl5rt4Xq8u-wBwPcb15sIYS2CN6d69zueM=' 19 | 20 | DATABASES = { 21 | 'default': { 22 | 'ENGINE': 'django.db.backends.sqlite3', 23 | 'NAME': 'testapp.sqlite3', 24 | } 25 | } 26 | # DATABASES = { 27 | # 'default': { 28 | # 'ENGINE': 'django.db.backends.postgresql_psycopg2', 29 | # 'USERNAME': 'dana', 30 | # 'HOST': '127.0.0.1', 31 | # 'PORT': 5432, 32 | # 'NAME': 'test', 33 | # } 34 | # } 35 | 36 | MIDDLEWARE_CLASSES = ( 37 | 'django.middleware.common.CommonMiddleware', 38 | 'django.middleware.csrf.CsrfViewMiddleware', 39 | ) 40 | -------------------------------------------------------------------------------- /testapp/models.py: -------------------------------------------------------------------------------- 1 | import django.db.models 2 | 3 | from cryptographic_fields import fields 4 | 5 | 6 | class TestModel(django.db.models.Model): 7 | enc_char_field = fields.EncryptedCharField(max_length=100) 8 | enc_text_field = fields.EncryptedTextField() 9 | enc_date_field = fields.EncryptedDateField(null=True) 10 | enc_date_now_field = fields.EncryptedDateField(auto_now=True, null=True) 11 | enc_date_now_add_field = fields.EncryptedDateField( 12 | auto_now_add=True, null=True) 13 | enc_datetime_field = fields.EncryptedDateTimeField(null=True) 14 | enc_boolean_field = fields.EncryptedBooleanField(default=True) 15 | enc_null_boolean_field = fields.EncryptedNullBooleanField() 16 | enc_integer_field = fields.EncryptedIntegerField(null=True) 17 | enc_positive_integer_field = fields.EncryptedPositiveIntegerField(null=True) 18 | enc_small_integer_field = fields.EncryptedSmallIntegerField(null=True) 19 | enc_positive_small_integer_field = \ 20 | fields.EncryptedPositiveSmallIntegerField(null=True) 21 | enc_big_integer_field = fields.EncryptedBigIntegerField(null=True) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 FounderTherapy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import re 5 | import os 6 | from setuptools import setup, find_packages 7 | 8 | 9 | with open('cryptographic_fields/__init__.py', 'r') as init_file: 10 | version = re.search( 11 | '^__version__ = [\'"]([^\'"]+)[\'"]', 12 | init_file.read(), 13 | re.MULTILINE, 14 | ).group(1) 15 | 16 | # allow setup.py to be run from any path 17 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 18 | 19 | setup( 20 | name='django-cryptographic-fields', 21 | version=version, 22 | packages=find_packages(), 23 | license='MIT', 24 | include_package_data=True, 25 | description=( 26 | 'A set of django fields that internally are encrypted using the ' 27 | 'cryptography.io native python encryption library.' 28 | ), 29 | url='http://github.com/foundertherapy/django-cryptographic-fields/', 30 | download_url='https://github.com/foundertherapy/django-cryptographic-fields/archive/' + version + '.tar.gz', 31 | author='Dana Spiegel', 32 | author_email='nasief304@gmail.com', 33 | install_requires=[ 34 | 'Django>=1.7', 35 | 'cryptography>=0.8.2', 36 | ], 37 | keywords=['encryption', 'django', 'fields', ], 38 | ) 39 | -------------------------------------------------------------------------------- /cryptographic_fields/tests.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.test import TestCase 4 | from django.core.exceptions import ImproperlyConfigured 5 | 6 | import cryptography.fernet 7 | 8 | import fields 9 | 10 | 11 | class TestSettings(TestCase): 12 | def setUp(self): 13 | self.key1 = cryptography.fernet.Fernet.generate_key() 14 | self.key2 = cryptography.fernet.Fernet.generate_key() 15 | 16 | def test_settings(self): 17 | with self.settings(FIELD_ENCRYPTION_KEY=self.key1): 18 | fields.get_crypter() 19 | 20 | with self.settings(FIELD_ENCRYPTION_KEY=(self.key1, self.key2,)): 21 | fields.get_crypter() 22 | 23 | with self.settings(FIELD_ENCRYPTION_KEY=[self.key1, self.key2, ]): 24 | fields.get_crypter() 25 | 26 | def test_settings_empty(self): 27 | with self.settings(FIELD_ENCRYPTION_KEY=None): 28 | self.assertRaises(ImproperlyConfigured, fields.get_crypter) 29 | 30 | with self.settings(FIELD_ENCRYPTION_KEY=''): 31 | self.assertRaises(ImproperlyConfigured, fields.get_crypter) 32 | 33 | with self.settings(FIELD_ENCRYPTION_KEY=[]): 34 | self.assertRaises(ImproperlyConfigured, fields.get_crypter) 35 | 36 | with self.settings(FIELD_ENCRYPTION_KEY=tuple()): 37 | self.assertRaises(ImproperlyConfigured, fields.get_crypter) 38 | 39 | def test_settings_bad(self): 40 | with self.settings(FIELD_ENCRYPTION_KEY=self.key1[:5]): 41 | self.assertRaises(ImproperlyConfigured, fields.get_crypter) 42 | 43 | with self.settings(FIELD_ENCRYPTION_KEY=(self.key1[:5], self.key2,)): 44 | self.assertRaises(ImproperlyConfigured, fields.get_crypter) 45 | 46 | with self.settings(FIELD_ENCRYPTION_KEY=[self.key1[:5], self.key2[:5], ]): 47 | self.assertRaises(ImproperlyConfigured, fields.get_crypter) 48 | -------------------------------------------------------------------------------- /testapp/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import cryptographic_fields.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='TestModel', 16 | fields=[ 17 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 18 | ('enc_char_field', cryptographic_fields.fields.EncryptedCharField(max_length=396)), 19 | ('enc_text_field', cryptographic_fields.fields.EncryptedTextField()), 20 | ('enc_date_field', cryptographic_fields.fields.EncryptedDateField(max_length=100, null=True)), 21 | ('enc_date_now_field', cryptographic_fields.fields.EncryptedDateField(auto_now=True, max_length=100, null=True)), 22 | ('enc_date_now_add_field', cryptographic_fields.fields.EncryptedDateField(auto_now_add=True, max_length=100, null=True)), 23 | ('enc_datetime_field', cryptographic_fields.fields.EncryptedDateTimeField(max_length=100, null=True)), 24 | ('enc_boolean_field', cryptographic_fields.fields.EncryptedBooleanField(default=True, max_length=100)), 25 | ('enc_null_boolean_field', cryptographic_fields.fields.EncryptedNullBooleanField(max_length=100)), 26 | ('enc_integer_field', cryptographic_fields.fields.EncryptedIntegerField(null=True)), 27 | ('enc_positive_integer_field', cryptographic_fields.fields.EncryptedPositiveIntegerField(null=True)), 28 | ('enc_small_integer_field', cryptographic_fields.fields.EncryptedSmallIntegerField(null=True)), 29 | ('enc_positive_small_integer_field', cryptographic_fields.fields.EncryptedPositiveSmallIntegerField(null=True)), 30 | ('enc_big_integer_field', cryptographic_fields.fields.EncryptedBigIntegerField(null=True)), 31 | ], 32 | options={ 33 | }, 34 | bases=(models.Model,), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django Cryptographic Fields 2 | =========================== 3 | 4 | .. image:: https://circleci.com/gh/foundertherapy/django-cryptographic-fields.png 5 | :target: https://circleci.com/gh/foundertherapy/django-cryptographic-fields 6 | 7 | About 8 | ----- 9 | 10 | ``django-cryptographic-fields`` is set of fields that wrap standard Django 11 | fields with encryption provided by the python cryptography library. These 12 | fields are much more compatible with a 12-factor design since they take their 13 | encryption key from the settings file instead of a file on disk used by 14 | ``keyczar``. 15 | 16 | While keyczar is an excellent tool to use for encryption, it's not compatible 17 | with Python 3, and it requires, for hosts like Heroku, that you either check 18 | your key file into your git repository for deployment, or implement manual 19 | post-deployment processing to write the key stored in an environment variable 20 | into a file that keyczar can read. 21 | 22 | Getting Started 23 | --------------- 24 | 25 | $ pip install django-cryptographic-fields 26 | 27 | Add "cryptographic_fields" to your INSTALLED_APPS setting like this: 28 | 29 | INSTALLED_APPS = ( 30 | ... 31 | 'cryptographic_fields', 32 | ) 33 | 34 | ``django-cryptographic-fields`` expects the encryption key to be specified 35 | using ``FIELD_ENCRYPTION_KEY`` in your project's ``settings.py`` file. For 36 | example, to load it from the local environment: 37 | 38 | import os 39 | 40 | FIELD_ENCRYPTION_KEY = os.environ.get('FIELD_ENCRYPTION_KEY', '') 41 | 42 | To use an encrypted field in a Django model, use one of the fields from the 43 | ``cryptographic_fields`` module: 44 | 45 | from cryptographic_fields.fields import EncryptedCharField 46 | 47 | class EncryptedFieldModel(models.Model): 48 | encrypted_char_field = EncryptedCharField(max_length=100) 49 | 50 | For fields that require ``max_length`` to be specified, the ``Encrypted`` 51 | variants of those fields will automatically increase the size of the database 52 | field to hold the encrypted form of the content. For example, a 3 character 53 | CharField will automatically specify a database field size of 100 characters 54 | when ``EncryptedCharField(max_length=3)`` is specified. 55 | 56 | Due to the nature of the encrypted data, filtering by values contained in 57 | encrypted fields won't work properly. Sorting is also not supported. 58 | 59 | Generating an Encryption Key 60 | ---------------------------- 61 | 62 | There is a Django management command ``generate_encryption_key`` provided 63 | with the ``cryptographic_fields`` library. Use this command to generate a new 64 | encryption key to set as ``settings.FIELD_ENCRYPTION_KEY``. 65 | 66 | ./manage.py generate_encryption_key 67 | 68 | Running this command will print an encryption key to the terminal, which can 69 | be configured in your environment or settings file. 70 | 71 | -------------------------------------------------------------------------------- /cryptographic_fields/fields.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import sys 4 | 5 | import django.db 6 | import django.db.models 7 | from django.utils.six import PY2, string_types 8 | from django.utils.functional import cached_property 9 | from django.core import validators 10 | from django.conf import settings 11 | from django.core.exceptions import ImproperlyConfigured 12 | 13 | import cryptography.fernet 14 | 15 | 16 | def get_crypter(): 17 | configured_keys = getattr(settings, 'FIELD_ENCRYPTION_KEY') 18 | 19 | if configured_keys is None: 20 | raise ImproperlyConfigured('FIELD_ENCRYPTION_KEY must be defined in settings') 21 | 22 | try: 23 | # Allow the use of key rotation 24 | if isinstance(configured_keys, (tuple, list)): 25 | keys = [cryptography.fernet.Fernet(str(k)) for k in configured_keys] 26 | else: 27 | # else turn the single key into a list of one 28 | keys = [cryptography.fernet.Fernet(str(configured_keys)), ] 29 | except Exception as e: 30 | raise ImproperlyConfigured( 31 | 'FIELD_ENCRYPTION_KEY defined incorrectly: {}'.format(str(e))), None, sys.exc_info()[2] 32 | 33 | if len(keys) == 0: 34 | raise ImproperlyConfigured('No keys defined in setting FIELD_ENCRYPTION_KEY') 35 | 36 | return cryptography.fernet.MultiFernet(keys) 37 | 38 | 39 | CRYPTER = get_crypter() 40 | 41 | 42 | def encrypt_str(s): 43 | # be sure to encode the string to bytes 44 | return CRYPTER.encrypt(s.encode('utf-8')) 45 | 46 | 47 | def decrypt_str(t): 48 | # be sure to decode the bytes to a string 49 | return CRYPTER.decrypt(t.encode('utf-8')).decode('utf-8') 50 | 51 | 52 | def calc_encrypted_length(n): 53 | # calculates the characters necessary to hold an encrypted string of 54 | # n bytes 55 | return len(encrypt_str('a' * n)) 56 | 57 | 58 | class EncryptedMixin(object): 59 | def to_python(self, value): 60 | if value is None: 61 | return value 62 | 63 | if isinstance(value, (bytes, string_types[0])): 64 | if isinstance(value, bytes): 65 | value = value.decode('utf-8') 66 | try: 67 | value = decrypt_str(value) 68 | except cryptography.fernet.InvalidToken: 69 | pass 70 | 71 | return super(EncryptedMixin, self).to_python(value) 72 | 73 | def from_db_value(self, value, expression, connection, context): 74 | return self.to_python(value) 75 | 76 | def get_db_prep_save(self, value, connection): 77 | value = super(EncryptedMixin, self).get_db_prep_save( 78 | value, connection) 79 | 80 | if value is None: 81 | return value 82 | if PY2: 83 | return encrypt_str(unicode(value)) 84 | # decode the encrypted value to a unicode string, else this breaks in pgsql 85 | return (encrypt_str(str(value))).decode('utf-8') 86 | 87 | def get_internal_type(self): 88 | return "TextField" 89 | 90 | def deconstruct(self): 91 | name, path, args, kwargs = super(EncryptedMixin, self).deconstruct() 92 | 93 | if 'max_length' in kwargs: 94 | del kwargs['max_length'] 95 | 96 | return name, path, args, kwargs 97 | 98 | 99 | class EncryptedCharField(EncryptedMixin, django.db.models.CharField): 100 | 101 | pass 102 | 103 | 104 | class EncryptedTextField(EncryptedMixin, django.db.models.TextField): 105 | pass 106 | 107 | 108 | class EncryptedDateField(EncryptedMixin, django.db.models.DateField): 109 | pass 110 | 111 | 112 | class EncryptedDateTimeField(EncryptedMixin, django.db.models.DateTimeField): 113 | pass 114 | 115 | 116 | class EncryptedEmailField(EncryptedMixin, django.db.models.EmailField): 117 | pass 118 | 119 | 120 | class EncryptedBooleanField(EncryptedMixin, django.db.models.BooleanField): 121 | 122 | def get_db_prep_save(self, value, connection): 123 | if value is None: 124 | return value 125 | if value is True: 126 | value = '1' 127 | elif value is False: 128 | value = '0' 129 | if PY2: 130 | return encrypt_str(unicode(value)) 131 | # decode the encrypted value to a unicode string, else this breaks in pgsql 132 | return encrypt_str(str(value)).decode('utf-8') 133 | 134 | 135 | class EncryptedNullBooleanField(EncryptedMixin, django.db.models.NullBooleanField): 136 | 137 | def get_db_prep_save(self, value, connection): 138 | if value is None: 139 | return value 140 | if value is True: 141 | value = '1' 142 | elif value is False: 143 | value = '0' 144 | if PY2: 145 | return encrypt_str(unicode(value)) 146 | # decode the encrypted value to a unicode string, else this breaks in pgsql 147 | return encrypt_str(str(value)).decode('utf-8') 148 | 149 | 150 | class EncryptedNumberMixin(EncryptedMixin): 151 | max_length = 20 152 | 153 | @cached_property 154 | def validators(self): 155 | # These validators can't be added at field initialization time since 156 | # they're based on values retrieved from `connection`. 157 | range_validators = [] 158 | internal_type = self.__class__.__name__[9:] 159 | min_value, max_value = django.db.connection.ops.integer_field_range( 160 | internal_type) 161 | if min_value is not None: 162 | range_validators.append(validators.MinValueValidator(min_value)) 163 | if max_value is not None: 164 | range_validators.append(validators.MaxValueValidator(max_value)) 165 | return super(EncryptedNumberMixin, self).validators + range_validators 166 | 167 | 168 | class EncryptedIntegerField(EncryptedNumberMixin, django.db.models.IntegerField): 169 | description = "An IntegerField that is encrypted before " \ 170 | "inserting into a database using the python cryptography " \ 171 | "library" 172 | pass 173 | 174 | 175 | class EncryptedPositiveIntegerField(EncryptedNumberMixin, django.db.models.PositiveIntegerField): 176 | pass 177 | 178 | 179 | class EncryptedSmallIntegerField(EncryptedNumberMixin, django.db.models.SmallIntegerField): 180 | pass 181 | 182 | 183 | class EncryptedPositiveSmallIntegerField(EncryptedNumberMixin, django.db.models.PositiveSmallIntegerField): 184 | pass 185 | 186 | 187 | class EncryptedBigIntegerField(EncryptedNumberMixin, django.db.models.BigIntegerField): 188 | pass 189 | -------------------------------------------------------------------------------- /testapp/tests.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import datetime 4 | import mock 5 | 6 | from django.forms import ModelForm 7 | from django.test import TestCase 8 | from django.utils import timezone 9 | 10 | import cryptography.fernet 11 | import cryptographic_fields.fields 12 | 13 | from . import models 14 | 15 | 16 | class TestModelTestCase(TestCase): 17 | def test_value(self): 18 | test_date_today = datetime.date.today() 19 | test_date = datetime.date(2011, 1, 1) 20 | test_datetime = datetime.datetime(2011, 1, 1, 1, tzinfo=timezone.utc) 21 | inst = models.TestModel() 22 | inst.enc_char_field = 'This is a test string!' 23 | inst.enc_text_field = 'This is a test string2!' 24 | inst.enc_date_field = test_date 25 | inst.enc_datetime_field = test_datetime 26 | inst.enc_boolean_field = True 27 | inst.enc_null_boolean_field = True 28 | inst.enc_integer_field = 123456789 29 | inst.enc_positive_integer_field = 123456789 30 | inst.enc_small_integer_field = 123456789 31 | inst.enc_positive_small_integer_field = 123456789 32 | inst.enc_big_integer_field = 9223372036854775807 33 | inst.save() 34 | 35 | inst = models.TestModel.objects.get() 36 | self.assertEqual(inst.enc_char_field, 'This is a test string!') 37 | self.assertEqual(inst.enc_text_field, 'This is a test string2!') 38 | self.assertEqual(inst.enc_date_field, test_date) 39 | self.assertEqual(inst.enc_date_now_field, test_date_today) 40 | self.assertEqual(inst.enc_date_now_add_field, test_date_today) 41 | # be careful about sqlite testing, which doesn't support native dates 42 | if timezone.is_naive(inst.enc_datetime_field): 43 | inst.enc_datetime_field = timezone.make_aware(inst.enc_datetime_field, timezone.utc) 44 | self.assertEqual(inst.enc_datetime_field, test_datetime) 45 | self.assertEqual(inst.enc_boolean_field, True) 46 | self.assertEqual(inst.enc_null_boolean_field, True) 47 | self.assertEqual(inst.enc_integer_field, 123456789) 48 | self.assertEqual(inst.enc_positive_integer_field, 123456789) 49 | self.assertEqual(inst.enc_small_integer_field, 123456789) 50 | self.assertEqual(inst.enc_positive_small_integer_field, 123456789) 51 | self.assertEqual(inst.enc_big_integer_field, 9223372036854775807) 52 | 53 | test_date = datetime.date(2012, 2, 1) 54 | test_datetime = datetime.datetime(2012, 1, 1, 2, tzinfo=timezone.utc) 55 | inst.enc_char_field = 'This is another test string!' 56 | inst.enc_text_field = 'This is another test string2!' 57 | inst.enc_date_field = test_date 58 | inst.enc_datetime_field = test_datetime 59 | inst.enc_boolean_field = False 60 | inst.enc_null_boolean_field = False 61 | inst.enc_integer_field = -123456789 62 | inst.enc_positive_integer_field = 0 63 | inst.enc_small_integer_field = -123456789 64 | inst.enc_positive_small_integer_field = 0 65 | inst.enc_big_integer_field = -9223372036854775806 66 | inst.save() 67 | 68 | inst = models.TestModel.objects.get() 69 | self.assertEqual(inst.enc_char_field, 'This is another test string!') 70 | self.assertEqual(inst.enc_text_field, 'This is another test string2!') 71 | self.assertEqual(inst.enc_date_field, test_date) 72 | self.assertEqual(inst.enc_date_now_field, datetime.date.today()) 73 | self.assertEqual(inst.enc_date_now_add_field, datetime.date.today()) 74 | # be careful about sqlite testing, which doesn't support native dates 75 | if timezone.is_naive(inst.enc_datetime_field): 76 | inst.enc_datetime_field = timezone.make_aware(inst.enc_datetime_field, timezone.utc) 77 | self.assertEqual(inst.enc_datetime_field, test_datetime) 78 | self.assertEqual(inst.enc_boolean_field, False) 79 | self.assertEqual(inst.enc_null_boolean_field, False) 80 | self.assertEqual(inst.enc_integer_field, -123456789) 81 | self.assertEqual(inst.enc_positive_integer_field, 0) 82 | self.assertEqual(inst.enc_small_integer_field, -123456789) 83 | self.assertEqual(inst.enc_positive_small_integer_field, 0) 84 | self.assertEqual(inst.enc_big_integer_field, -9223372036854775806) 85 | 86 | inst.enc_null_boolean_field = None 87 | inst.save() 88 | inst = models.TestModel.objects.get() 89 | self.assertEqual(inst.enc_null_boolean_field, None) 90 | 91 | def test_unicode_value(self): 92 | inst = models.TestModel() 93 | inst.enc_char_field = u'\xa2\u221e\xa7\xb6\u2022\xaa' 94 | inst.enc_text_field = u'\xa2\u221e\xa7\xb6\u2022\xa2' 95 | inst.save() 96 | 97 | inst2 = models.TestModel.objects.get() 98 | self.assertEqual(inst2.enc_char_field, u'\xa2\u221e\xa7\xb6\u2022\xaa') 99 | self.assertEqual(inst2.enc_text_field, u'\xa2\u221e\xa7\xb6\u2022\xa2') 100 | 101 | @mock.patch('django.db.models.sql.compiler.SQLCompiler.get_converters') 102 | def test_raw_value(self, get_converters_method): 103 | get_converters_method.return_value = [] 104 | 105 | inst = models.TestModel() 106 | inst.enc_char_field = 'This is a test string!' 107 | inst.enc_text_field = 'This is a test string2!' 108 | inst.enc_date_field = datetime.date(2011, 1, 1) 109 | inst.enc_datetime_field = datetime.datetime(2012, 2, 1, 1, tzinfo=timezone.UTC()) 110 | inst.enc_boolean_field = True 111 | inst.enc_null_boolean_field = True 112 | inst.enc_integer_field = 123456789 113 | inst.enc_positive_integer_field = 123456789 114 | inst.enc_small_integer_field = 123456789 115 | inst.enc_positive_small_integer_field = 123456789 116 | inst.enc_big_integer_field = 9223372036854775807 117 | inst.save() 118 | 119 | d = models.TestModel.objects.values()[0] 120 | for key, value in d.items(): 121 | if key == 'id': 122 | continue 123 | self.assertEqual(value[:7], 'gAAAAAB', '{} failed: {}'.format(key, value)) 124 | 125 | inst.enc_null_boolean_field = None 126 | inst.save() 127 | 128 | d = models.TestModel.objects.values()[0] 129 | self.assertEqual(d['enc_null_boolean_field'], None) 130 | 131 | def test_get_internal_type(self): 132 | enc_char_field = models.TestModel._meta.fields[1] 133 | enc_text_field = models.TestModel._meta.fields[2] 134 | enc_date_field = models.TestModel._meta.fields[3] 135 | enc_date_now_field = models.TestModel._meta.fields[4] 136 | enc_boolean_field = models.TestModel._meta.fields[7] 137 | enc_null_boolean_field = models.TestModel._meta.fields[8] 138 | enc_integer_field = models.TestModel._meta.fields[9] 139 | enc_positive_integer_field = models.TestModel._meta.fields[10] 140 | enc_small_integer_field = models.TestModel._meta.fields[11] 141 | enc_positive_small_integer_field = models.TestModel._meta.fields[12] 142 | enc_big_integer_field = models.TestModel._meta.fields[13] 143 | 144 | self.assertEqual(enc_char_field.get_internal_type(), 'TextField') 145 | self.assertEqual(enc_text_field.get_internal_type(), 'TextField') 146 | self.assertEqual(enc_date_field.get_internal_type(), 'TextField') 147 | self.assertEqual(enc_date_now_field.get_internal_type(), 'TextField') 148 | self.assertEqual(enc_boolean_field.get_internal_type(), 'TextField') 149 | self.assertEqual(enc_null_boolean_field.get_internal_type(), 'TextField') 150 | 151 | self.assertEqual(enc_integer_field.get_internal_type(), 'TextField') 152 | self.assertEqual(enc_positive_integer_field.get_internal_type(), 'TextField') 153 | self.assertEqual(enc_small_integer_field.get_internal_type(), 'TextField') 154 | self.assertEqual(enc_positive_small_integer_field.get_internal_type(), 'TextField') 155 | self.assertEqual(enc_big_integer_field.get_internal_type(), 'TextField') 156 | 157 | def test_auto_date(self): 158 | enc_date_now_field = models.TestModel._meta.fields[4] 159 | self.assertEqual(enc_date_now_field.name, 'enc_date_now_field') 160 | self.assertTrue(enc_date_now_field.auto_now) 161 | 162 | enc_date_now_add_field = models.TestModel._meta.fields[5] 163 | self.assertEqual(enc_date_now_add_field.name, 'enc_date_now_add_field') 164 | self.assertFalse(enc_date_now_add_field.auto_now) 165 | 166 | self.assertFalse(enc_date_now_field.auto_now_add) 167 | self.assertTrue(enc_date_now_add_field.auto_now_add) 168 | 169 | def test_max_length_validation(self): 170 | class TestModelForm(ModelForm): 171 | class Meta: 172 | model = models.TestModel 173 | fields = ('enc_char_field', ) 174 | 175 | f = TestModelForm(data={'enc_char_field': 'a' * 200}) 176 | self.assertFalse(f.is_valid()) 177 | 178 | f = TestModelForm(data={'enc_char_field': 'a' * 99}) 179 | self.assertTrue(f.is_valid()) 180 | 181 | def test_rotating_keys(self): 182 | key1 = cryptography.fernet.Fernet.generate_key() 183 | key2 = cryptography.fernet.Fernet.generate_key() 184 | 185 | with self.settings(FIELD_ENCRYPTION_KEY=key1): 186 | # make sure we update the crypter with the new key 187 | cryptographic_fields.fields.CRYPTER = cryptographic_fields.fields.get_crypter() 188 | 189 | test_date_today = datetime.date.today() 190 | test_date = datetime.date(2011, 1, 1) 191 | test_datetime = datetime.datetime(2011, 1, 1, 1, tzinfo=timezone.utc) 192 | inst = models.TestModel() 193 | inst.enc_char_field = 'This is a test string!' 194 | inst.enc_text_field = 'This is a test string2!' 195 | inst.enc_date_field = test_date 196 | inst.enc_datetime_field = test_datetime 197 | inst.enc_boolean_field = True 198 | inst.enc_null_boolean_field = True 199 | inst.enc_integer_field = 123456789 200 | inst.enc_positive_integer_field = 123456789 201 | inst.enc_small_integer_field = 123456789 202 | inst.enc_positive_small_integer_field = 123456789 203 | inst.enc_big_integer_field = 9223372036854775807 204 | inst.save() 205 | 206 | # test that loading the instance from the database results in usable data 207 | # (since it uses the older key that's still configured) 208 | with self.settings(FIELD_ENCRYPTION_KEY=[key2, key1]): 209 | # make sure we update the crypter with the new key 210 | cryptographic_fields.fields.CRYPTER = cryptographic_fields.fields.get_crypter() 211 | 212 | inst = models.TestModel.objects.get() 213 | self.assertEqual(inst.enc_char_field, 'This is a test string!') 214 | self.assertEqual(inst.enc_text_field, 'This is a test string2!') 215 | self.assertEqual(inst.enc_date_field, test_date) 216 | self.assertEqual(inst.enc_date_now_field, test_date_today) 217 | self.assertEqual(inst.enc_date_now_add_field, test_date_today) 218 | # be careful about sqlite testing, which doesn't support native dates 219 | if timezone.is_naive(inst.enc_datetime_field): 220 | inst.enc_datetime_field = timezone.make_aware(inst.enc_datetime_field, timezone.utc) 221 | self.assertEqual(inst.enc_datetime_field, test_datetime) 222 | self.assertEqual(inst.enc_boolean_field, True) 223 | self.assertEqual(inst.enc_null_boolean_field, True) 224 | self.assertEqual(inst.enc_integer_field, 123456789) 225 | self.assertEqual(inst.enc_positive_integer_field, 123456789) 226 | self.assertEqual(inst.enc_small_integer_field, 123456789) 227 | self.assertEqual(inst.enc_positive_small_integer_field, 123456789) 228 | self.assertEqual(inst.enc_big_integer_field, 9223372036854775807) 229 | 230 | # save the instance to rotate the key 231 | inst.save() 232 | 233 | # test that saving the instance results in key rotation to the correct key 234 | with self.settings(FIELD_ENCRYPTION_KEY=[key2, ]): 235 | # make sure we update the crypter with the new key 236 | cryptographic_fields.fields.CRYPTER = cryptographic_fields.fields.get_crypter() 237 | 238 | # test that loading the instance from the database results in usable data 239 | # (since it uses the older key that's still configured) 240 | inst = models.TestModel.objects.get() 241 | self.assertEqual(inst.enc_char_field, 'This is a test string!') 242 | self.assertEqual(inst.enc_text_field, 'This is a test string2!') 243 | self.assertEqual(inst.enc_date_field, test_date) 244 | self.assertEqual(inst.enc_date_now_field, test_date_today) 245 | self.assertEqual(inst.enc_date_now_add_field, test_date_today) 246 | # be careful about sqlite testing, which doesn't support native dates 247 | if timezone.is_naive(inst.enc_datetime_field): 248 | inst.enc_datetime_field = timezone.make_aware(inst.enc_datetime_field, timezone.utc) 249 | self.assertEqual(inst.enc_datetime_field, test_datetime) 250 | self.assertEqual(inst.enc_boolean_field, True) 251 | self.assertEqual(inst.enc_null_boolean_field, True) 252 | self.assertEqual(inst.enc_integer_field, 123456789) 253 | self.assertEqual(inst.enc_positive_integer_field, 123456789) 254 | self.assertEqual(inst.enc_small_integer_field, 123456789) 255 | self.assertEqual(inst.enc_positive_small_integer_field, 123456789) 256 | self.assertEqual(inst.enc_big_integer_field, 9223372036854775807) 257 | 258 | # test that the instance with rotated key is no longer readable using the old key 259 | with self.settings(FIELD_ENCRYPTION_KEY=[key1, ]): 260 | # make sure we update the crypter with the new key 261 | cryptographic_fields.fields.CRYPTER = cryptographic_fields.fields.get_crypter() 262 | 263 | # test that loading the instance from the database results in usable data 264 | # (since it uses the older key that's still configured) 265 | # Note we need to only load the enc_char_field because loading date field types results in conversion to python dates, 266 | # which will be raise a ValidationError when the field can't be properly decoded 267 | inst = models.TestModel.objects.only('enc_char_field').get() 268 | self.assertNotEqual(inst.enc_char_field, 'This is a test string!') 269 | self.assertEqual(inst.enc_char_field[:5], 'gAAAA') 270 | 271 | # reset the CRYPTER since we screwed with the default configuration with this test 272 | cryptographic_fields.fields.CRYPTER = cryptographic_fields.fields.get_crypter() 273 | 274 | --------------------------------------------------------------------------------