├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── drf_encrypt_content ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── generate_key.py ├── mixins.py ├── rest_encrypt_content.py ├── serializers.py └── tests │ └── __init__.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.db 3 | *~ 4 | .* 5 | 6 | /site/ 7 | /htmlcov/ 8 | /coverage/ 9 | /build/ 10 | /dist/ 11 | /*.egg-info/ 12 | /env/ 13 | MANIFEST 14 | coverage.* 15 | 16 | !.gitignore 17 | !.travis.yml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Oğuzhan Çelikarslan 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | <<<<<<< HEAD 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | ======= 32 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | >>>>>>> 3a4f958b5bd1598d5a398c7bbd78eb998d723b6a 34 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # drf-encrypt-content 2 | 3 | drf-encrypt-content is a Django app to help you encrypt your data, serialized through ModelSerializer. It also contains some helper functions. Which helps you to encrypt your data. drf-encrypt-content is built on top of the **cryptography** package. This package makes fernet available to us. 4 | 5 | ##### Personal Warning 6 | Because I don't have enough time, I could not add tests yet. 7 | I just tested it by hand based on Django 3.x and Django REST framework 3.11. 8 | As soon as I find some free time. I am gonna add tests and some feature requests. I am open to criticism. 9 | This is the first package that I made available to the Django community so looking forward to your comments to improve myself. 10 | 11 | ##### TODO 12 | * Tests 13 | * Code refactoring. 14 | * Helper methods to encrypt various data structures. 15 | 16 | ### What is the Fernet? 17 | 18 | Fernet guarantees that a message encrypted using it cannot be manipulated or read without the key. 19 | Fernet is an implementation of symmetric (also known as “secret key”) authenticated cryptography. 20 | 21 | ### Quick start 22 | 23 | * Add "drf_encrypt_content" to your INSTALLED_APPS setting like this. 24 | 25 | ``` 26 | INSTALLED_APPS = [ 27 | ... 28 | 'drf_encrypt_content', 29 | ] 30 | ```` 31 | 32 | * Jump terminal and cd where manage.py is. 33 | 34 | * Run ``python manage.py generate_key`` to get a fresh fernet key and copy the key. 35 | 36 | * Open settings.py file and provide the below variable. 37 | 38 | `` REST_ENCRYPT_SECRET_KEY = 'key_you_copied_at_step_3' `` 39 | 40 | * Open the file where your serializer using ModelSerializer stays and import and then use this mixin. 41 | 42 | ```python 43 | from drf_encrypt_content import RestEncryptContentMixin 44 | class MySerializer(RestEncryptContentMixin, ModelSerializer) 45 | ... 46 | ``` 47 | 48 | That is it. This is going to encrypt all of your exposed data. 49 | 50 | ### Installation 51 | 52 | ```bash 53 | pip install drf-encrypt-content 54 | ``` 55 | 56 | ### Usage 57 | This package makes you available three class **EncryptedModelSerializer**, **RestEncryptContentMixin** and **RestEncryptContent** 58 | 59 | 60 | **RestEncryptContentMixin**: This is the mixin where the magic happens. You basically need to provide this mixin to the class where you inherited ModelSerializer. This is going to **encrypt all of the data** ModelSerializer serialize by default. 61 | 62 | You can specify which fields in your model you want to encrypt. Just define a list with the name 'encrypted_fields' in the class Meta where you define your model and fields. 63 | 64 | from drf_encrypt_content import RestEncryptContentMixin 65 | class MySerializer(RestEncryptContentMixin, ModelSerializer) 66 | class Meta: 67 | model = Model 68 | fields = '__all__' 69 | encrypted_fields = [ 70 | 'field_name' 71 | ] 72 | 73 | Instead of typing all of the fields by one by, you can type only the ones you don't want to encrypt. 74 | 75 | from drf_encrypt_content import RestEncryptContentMixin 76 | class MySerializer(RestEncryptContentMixin, ModelSerializer) 77 | class Meta: 78 | model = Model 79 | fields = '__all__' 80 | excluded_fields = [ 81 | 'field_name' 82 | ] 83 | 84 | 85 | **EncryptedModelSerializer**: If you want to use model serializer and also RestEncryptContentMixin mixin, change your ModelSerializer with EncryptedModelSerializer. EncryptedModelSerializer is based on ModelSerializer and RestEncryptContentMixin. You can do everything just like you do with Rest Encrypt Content Mixin. 86 | 87 | from drf_encrypt_content import EncryptedModelSerializer 88 | class MySerializer(EncryptedModelSerializer) 89 | class Meta: 90 | model = Model 91 | fields = '__all__' 92 | excluded_fields = [ 93 | 'field_name' 94 | ] 95 | 96 | ### What if I use base Serializer class? 97 | I do not support base Serializer yet but you can find some helper methods in **RestEncryptContent** class. 98 | 99 | You can use the below methods. 100 | 101 | **encrypt_list:** iterates an unencrypted list and encrypt items in the list and returns a list of encrypted data. 102 | 103 | from drf_encrypt_content import RestEncryptContent 104 | list_one = [1,2,3] 105 | rest_encrypt = RestEncryptContent() 106 | rest_enctypt.encrypt_list(data) # a list of encrypted data. 107 | 108 | **encrypt_data:** returns fernet token which is the encrypted form of passed data. 109 | 110 | from drf_encrypt_content import RestEncryptContent 111 | data = 'example_content' 112 | rest_encrypt = RestEncryptContent() 113 | rest_enctypt.encrypt_data(data) # this is going to return encrypted data. 114 | 115 | 116 | ### License 117 | BSD 118 | 119 | ### Author 120 | Oğuzhan Çelikarslan 121 | 122 | ### Special Thanks to 123 | Developing this package, I read through some codes and articles. I want to say thanks to persons who wrote and coded these. 124 | 125 | Gajesh Naik 126 | 127 | Tom Christie 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /drf_encrypt_content/__init__.py: -------------------------------------------------------------------------------- 1 | __title__ = 'DRF encrypt content' 2 | __version__ = '0.1.0' 3 | __author__ = 'oguzhancelikarslan' 4 | __license__ = 'BSD 2-Clause' 5 | 6 | # Version synonym 7 | VERSION = __version__ 8 | 9 | from .mixins import RestEncryptContentMixin 10 | from .rest_encrypt_content import RestEncryptContent 11 | from .serializers import EncryptedModelSerializer 12 | -------------------------------------------------------------------------------- /drf_encrypt_content/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oguzhancelikarslan/drf-encrypt-content/ec9912dd0938d727374c7215bfc3c5040322e72c/drf_encrypt_content/management/__init__.py -------------------------------------------------------------------------------- /drf_encrypt_content/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oguzhancelikarslan/drf-encrypt-content/ec9912dd0938d727374c7215bfc3c5040322e72c/drf_encrypt_content/management/commands/__init__.py -------------------------------------------------------------------------------- /drf_encrypt_content/management/commands/generate_key.py: -------------------------------------------------------------------------------- 1 | from cryptography.fernet import Fernet 2 | from django.core.management import BaseCommand 3 | 4 | 5 | class Command(BaseCommand): 6 | """ 7 | We have to keep this key in a safe place. 8 | if we lose it, we cannot decrypt data encrypted with this key. 9 | """ 10 | 11 | help = 'Creates new key to encrypt data.' 12 | 13 | def handle(self, *args, **options): 14 | key = Fernet.generate_key() 15 | self.stdout.write(self.style.WARNING( 16 | 'Keep this key in a safe place. if you lose the key, ' 17 | 'you will no longer be able to decrypt data that was encrypted with this key.') 18 | ) 19 | self.stdout.write('Key: %s' % key.decode("utf-8")) 20 | self.stdout.write(self.style.SUCCESS('Successfully generated')) 21 | -------------------------------------------------------------------------------- /drf_encrypt_content/mixins.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from django.core.exceptions import ImproperlyConfigured 4 | 5 | from drf_encrypt_content.rest_encrypt_content import RestEncryptContent 6 | 7 | 8 | class RestEncryptContentMixin(RestEncryptContent): 9 | __ALL__ = '__all__' 10 | 11 | def to_representation(self, instance): 12 | representation = super(RestEncryptContentMixin, self).to_representation(instance) 13 | encrypted_fields = getattr(self.Meta, 'encrypted_fields', self.__ALL__) 14 | excluded_fields = getattr(self.Meta, 'excluded_fields', None) 15 | fields_list = list() 16 | model_class = getattr(self.Meta, 'model') 17 | 18 | if encrypted_fields and encrypted_fields != self.__ALL__ and not isinstance(encrypted_fields, (list, tuple)): 19 | raise TypeError( 20 | 'The `encrypted_fields` option must be a list or tuple or "__all__". ' 21 | 'Got %s.' % type(encrypted_fields).__name__ 22 | ) 23 | 24 | if excluded_fields and not isinstance(excluded_fields, (list, tuple)): 25 | raise TypeError( 26 | 'The `excluded_fields` option must be a list or tuple. Got %s.' % 27 | type(excluded_fields).__name__ 28 | ) 29 | 30 | assert not ((encrypted_fields != self.__ALL__) and excluded_fields), ( 31 | "Cannot set both 'encrypted_fields' and 'excluded_fields' options on " 32 | "serializer {serializer_class}.".format( 33 | serializer_class=self.__class__.__name__ 34 | ) 35 | ) 36 | 37 | if encrypted_fields == self.__ALL__: 38 | fields_list = [key for key, value in representation.items()] 39 | else: 40 | for field in encrypted_fields: 41 | if not (field in representation.keys()): 42 | raise ImproperlyConfigured( 43 | 'Field name `%s` is not valid for model `%s`.' % 44 | (field, model_class.__name__) 45 | ) 46 | 47 | for key in representation.keys(): 48 | if key in encrypted_fields: 49 | fields_list.append(key) 50 | 51 | if excluded_fields is not None: 52 | for field in excluded_fields: 53 | if not (field in fields_list): 54 | raise ImproperlyConfigured( 55 | 'Field name `%s` is not valid for model `%s`.' % 56 | (field, model_class.__name__) 57 | ) 58 | else: 59 | fields_list.remove(field) 60 | 61 | for key, value in representation.items(): 62 | if key in fields_list: 63 | if type(representation[key]) is not OrderedDict: 64 | representation[key] = self.encrypt_data(str(value)) 65 | 66 | return representation 67 | -------------------------------------------------------------------------------- /drf_encrypt_content/rest_encrypt_content.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from cryptography.fernet import Fernet 3 | 4 | 5 | class RestEncryptContent: 6 | """ 7 | we can transfer data in the encrypted form using symmetric encryption. 8 | the parties who has the key are able to decrypt the encrypted data. 9 | """ 10 | try: 11 | REST_ENCRYPT_SECRET_KEY = getattr(settings, 'REST_ENCRYPT_SECRET_KEY') 12 | except AttributeError as e: 13 | e.args = ('Provide the REST_ENCRYPT_SECRET_KEY in settings with the key generated by generate_key command.',) 14 | raise 15 | 16 | fernet = Fernet(REST_ENCRYPT_SECRET_KEY) 17 | 18 | def encrypt_data(self, data: str): 19 | """ 20 | returns: fernet token which is the encrypted form of passed data. 21 | """ 22 | return self.fernet.encrypt(self.encode_data(data)) 23 | 24 | def encode_data(self, data: str): 25 | """ 26 | converting strings to bytes is suitable for encryption, 27 | encode() method encodes data using utf-8 codec. 28 | """ 29 | return data.encode() 30 | 31 | def encrypt_list(self, unencrypted: list): 32 | """ 33 | iterates an unencrypted list and encrypt items in the list. 34 | returns: a list of encrypted data. 35 | """ 36 | temp_list = list() 37 | for data in unencrypted: 38 | temp_list.append(self.encrypt_data(str(data))) 39 | 40 | return temp_list 41 | -------------------------------------------------------------------------------- /drf_encrypt_content/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from .mixins import RestEncryptContentMixin 4 | 5 | 6 | class EncryptedModelSerializer(RestEncryptContentMixin, serializers.ModelSerializer): 7 | pass 8 | -------------------------------------------------------------------------------- /drf_encrypt_content/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oguzhancelikarslan/drf-encrypt-content/ec9912dd0938d727374c7215bfc3c5040322e72c/drf_encrypt_content/tests/__init__.py -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = drf-encrypt-content 3 | version = 0.1 4 | description = The package helps you encrypt your serialized data. 5 | long_description = file: README.rst 6 | url = https://www.oguzhancelikarslan.com/ 7 | author = Oguzhan Celikarslan 8 | author_email = oguzhancelikarslan@gmail.com 9 | license = BSD 3-Clause "New" or "Revised" License 10 | classifiers = 11 | Environment :: Web Environment 12 | Framework :: Django 13 | Intended Audience :: Developers 14 | License :: OSI Approved :: BSD License 15 | Operating System :: OS Independent 16 | Framework :: Django :: 2.0 17 | Framework :: Django :: 2.1 18 | Framework :: Django :: 2.2 19 | Framework :: Django :: 3.0 20 | Framework :: Django :: 3.0 21 | Programming Language :: Python 22 | Programming Language :: Python :: 3 23 | Programming Language :: Python :: 3 :: Only 24 | Programming Language :: Python :: 3.6 25 | Programming Language :: Python :: 3.7 26 | Programming Language :: Python :: 3.8 27 | Programming Language :: Python :: 3.9 28 | Topic :: Internet :: WWW/HTTP 29 | Topic :: Internet :: WWW/HTTP :: Dynamic Content 30 | 31 | [options] 32 | include_package_data = true 33 | packages = find: -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | from os import path 4 | 5 | this_directory = path.abspath(path.dirname(__file__)) 6 | with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: 7 | long_description = f.read() 8 | 9 | setup( 10 | description=( 11 | 'The package helps you encrypt your serialized data.' 12 | ), 13 | long_description=long_description, 14 | long_description_content_type='text/markdown', 15 | install_requires=[ 16 | 'cryptography>=2.9.2', 17 | ], 18 | ) 19 | --------------------------------------------------------------------------------