Encrypting a Django Rest Framework Response
Tags: django, python, tutorial, webdev
Published: 2023-03-27 19:51:06

## Introduction
Encryption is the process data goes through to get transformed from a readable format(plaintext) to an unreadable format(ciphertext). After encryption, the ciphertext appears to be unreadable random data and anyone attempting to read the original data would require an encryption/decryption key. The purpose of encryption is to help protect sensitive information from unauthorized entities and to also enhance secure communication between two entities. In this article, we will talk about encryption and how we can handle response encryption in Django Rest Framework(DRF) with a detailed step on how to achieve this.

### Why encrypting DRF responses with AES is important?
Encrypting responses is important to protect sensitive data from being accessed or intercepted by unauthorized parties. By encrypting the response with a strong encryption algorithm like AES, the data can only be accessed by authorized parties with the correct key.

## AES Encryption
Advanced Encryption Standard (AES) is a specification for data encryption by the National Institute of Standards and Technology (NIST). AES uses a block cipher that works on fixed block sizes of 128 bits and has three key sizes: 128 bits, 192 bits, and 256 bits. The larger the key size, the stronger the encryption. AES encryption works by repeatedly applying a complex set of mathematical operations to the plaintext data, using the encryption key as input. This process, called rounds, converts the plaintext into ciphertext.

### Setting Up AES Encryption in DRF
Now I am going to walk you through setting up AES encryption in DRF and ensuring that all responses from your DRF APIs are encrypted.

Let us assume we work for the Secret Service and we need to build an API that fetches information about our agents and returns this information as a response, this is sensitive information so we need to encrypt this to ensure that even if the transmitted data is intercepted it will make no sense to the unauthorized party. We will build the project from scratch, create an endpoint that returns a list of agents details in plaintext format, create another endpoint that returns the data as ciphertext and another on that accepts the ciphertext and decrypts it to plaintext. You can follow along with the following outlined steps and note that a repository of the completed project can be found at this [github repository](https://github.com/Charlesu49/Django-Restframework-Response_Encryption).

#### Code Along

- Create a new project directory `mkdir encryption_project`.
- Change the directory into the newly created directory `cd encryption_project`.
- Create a virtual environment `virtualenv venv`.
- Activate the virtual environment `source venv/bin/activate`.
- Download Django, DRF, Pycryptodome(which provides the AES encryption class) and python-dotenv by running the following command `pip install django && pip install djangorestframework && pip install pycryptodome && pip install python-dotenv`.
- Add this to the requirements.txt file by running `pip freeze > requirements.txt`.
- Start a new Django project `django-admin startproject secret_service`.
- Create a new Django app `django-admin startapp agents`.
- Navigate to your settings.py file under the secret_service project directory and add the following line to register the newly created app(agents) and DRF;

    ```python
    # secret_service/settings.py
    INSTALLED_APPS = [
        ...
        # newly added below
        'rest_framework',
        'agents'
    ]
    ```
- Navigate to the models.py file in the agents app directory and create a model for the agent's information;

    ```python
    # agents/models.py

    from django.db import models

    class Agent(models.Model):
        first_name = models.CharField(max_length=100)
        last_name = models.CharField(max_length=100)
        badge_no = models.CharField(max_length=10, unique=True)
        code_name = models.CharField(max_length=100)
        location = models.CharField(max_length=100)
        secret_mission = models.TextField()
        active = models.BooleanField()
        no_of_assignments = models.IntegerField()
    ```

- Create a serializer for the Agent model, create a file named serializers.py under the agents app directory and add the below code;

    ```python
    # agents/serializers.py
    from rest_framework import serializers
    from .models import Agent

    class AgentSerializer(serializers.ModelSerializer):
        class Meta:
            model = Agent
            fields = ('first_name', 'last_name', 'badge_no', 'code_name', 'location', 'secret_mission')
    ```


    A serializer is a tool in Django that converts data from your application into a format that can be easily shared with other applications, usually in JSON format. In this example, the `AgentSerializer` is used to convert Agent model instances into JSON format so that they can be returned as API responses(note that we will be encrypting this json before returning it). The serializer defines which fields from the model should be included in the JSON output.


- Set up the Agent model to be managed through the Django admin site.

    ```python
    # agents/admin.py
    from django.contrib import admin
    from .models import Agent

    class AgentAdmin(admin.ModelAdmin):
        list_display = ("first_name", "last_name", "code_name", "location", "secret_mission")

    admin.site.register(Agent, AgentAdmin)
    ```



    The `AgentAdmin` class customizes the appearance of the Agent model in the admin site by defining which fields to display in the list view (`list_display`). The `admin.site.register(Agent, AgentAdmin)` line registers the `Agent` model with the admin site and applies the customizations defined in the `AgentAdmin` class.


- At this point, we can make migrations by running `python manage.py makemigrations` on the terminal to create the table on our database. Note that I am using the default Sqlite DB so this command automatically creates the database if you are using a non-file based database like Postgres/Oracle DB for instance you will have to create your database and manually configure the connection to it in your settings.py file in the project directory. Running this command should generate a response like the one below confirming that a new migrations file has been created;


    ![Make Migrations](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eyildl29hxm0l5baz9n4.png)

- Now we run `python manage.py migrate` to apply the created migrations, we should have a file like the below if it runs successfully;


    ![Migrate](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bykrt5tr8ta661jv26hg.png)

- We create a superuser to enable us to access the admin dashboard and add some dummy data to our agents table by running `python manage.py createsuperuser` and populating our data as below;


    ![createsuperuser command](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6s268109efdtw5m7j97p.png)

- We can start our development server by running `python manage.py runserver`.
- Navigate to your admin dashboard via the URL http://localhost/admin and sign in with the credentials created earlier. You should see the Agents models and a click on '+Add' should bring up a page that looks like the one below;


    ![Add Agent Data Form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5wnoe4bykm3ipwyyr2e7.png)

    Populate this page to maintain some dummy agents' data.


    ![Dummy Agent Data](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gt8csiqctunhmzzsy54t.png)


- Let us create a view that returns the agents information in plain text

    * In views.py file in the agents directory, add the below codeagents/views.py

        ```python
        # agents/views.py
        from rest_framework.views import APIView 
        from rest_framework.response import Response 
        from rest_framework import status 
        from .serializers import AgentSerializer 
        from .models import Agent 

        class AgentListPlainView(APIView):
            def get(self, request):
                agents = Agent.objects.all()
                if agents:
                    serializer = AgentSerializer(agents, many=True)
                    data = serializer.data
                data = {
                    'status': 'success',
                    'code' : status.HTTP_200_OK,
                    'data': data
                    }
                return Response(data, status=status.HTTP_200_OK)
        ```

        In DRF, the default renderer is `JSONRenderer`. This renderer is responsible for converting Python objects to JSON format before sending the response to the client. It is used when no other renderer is specified or when the `Accept` header of the request explicitly specifies `application/json` as the expected response format and because we wish to return this particular response as plaintext we will let the default renderer handle this.

        
    * Next, we register our app's URL routes in the urls.py file in the project directory by adding the below code;

        ```python
        # secret_service/urls.py
        
        from django.contrib import admin
        from django.urls import path, include
        
        urlpatterns = [
            path('admin/', admin.site.urls),
        
            # newly added line below
            path('', include('agents.urls'))
        ]
        ```
        
    * Next, we proceed to our *agents* directory and add a new file named *urls.py*
        
    * Navigate to the newly created *urls.py* file and add the below lines of code to register our endpoint for fetching agents' lists as plain text.
        
        ```python
        # agents/urls.py
        
        from django.urls import path
        from . import views
        
        urlpatterns = [
            path('agent-list-plain/', views.AgentListPlainView.as_view(), name='agent-list-plain'),
        ]
        ```
        
    * At this point, we can use postman, insomnia or curl to test our API using the URL http://localhost:8000/agent-list-plain/


        ![Plaintext response](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wpf5jkeojm8nbcas6z11.png)

        This is returned as plaintext because we are making use of DRF's default renderer in our view, while this may serve in many cases where encryption is not required, that is not our case here as we need to ensure the returned data is encrypted.
        
    * We can now create an endpoint that overrides DRF's default `JSONRenderer` with a custom renderer we will build following the below steps;
    
    * In the root of your project add a .env file and add the lines;
        
        ```bash
        # .env
        AES_IV=whsbdhgntkgngmhk
        AES_SECRET_KEY=somerandomsecret
        ```
        
        Ensure that the key used is **16, 24 or 32 bytes long** (respectively for AES-128, AES-192 or AES-256) with AES-256 being the most secure since it employs more rounds resulting in more complex encryption than the others.
        
    * In the *agents* directory create a new file renderers.py and add the below;
        
        ```python
        # agents/renderers.py
        
        import base64
        from Crypto.Cipher import AES
        from Crypto.Util.Padding import pad
        from rest_framework.renderers import BaseRenderer
        import json
        
        load_dotenv()
        
        # here the string gotten from the environmental variable is converted to bytes
        AES_SECRET_KEY = bytes(os.getenv('AES_SECRET_KEY'), 'utf-8')
        AES_IV = bytes(os.getenv('AES_IV'), 'utf-8')
        
        class CustomAesRenderer(BaseRenderer):
            media_type = 'application/octet-stream'
            format = 'aes'
        
            def render(self, data, media_type=None, renderer_context=None):
                plaintext = json.dumps(data)
                padded_plaintext = pad(plaintext.encode(), 16)
                cipher = AES.new(AES_SECRET_KEY, AES.MODE_CBC, AES_IV)
                ciphertext = cipher.encrypt(padded_plaintext)
                ciphertext_b64 = base64.b64encode(ciphertext).decode()
                response = {'ciphertext': ciphertext_b64}
                return json.dumps(response)
        ```
        
        Here we define a custom renderer for DRF that can be used to encrypt responses using the AES encryption algorithm. The `CustomAesRenderer` class is defined as a subclass of DRF's `BaseRenderer` class. It sets the `media_type` attribute to `application/octet-stream` and the `format` attribute to `aes`, indicating that this renderer will be used to render responses in binary format using AES encryption.


        The `render()` method is where the actual encryption takes place. It takes the data that needs to be encrypted as an argument, which is expected to be in JSON format. The data is first serialized to JSON format using the `json.dumps()` method. The serialized data is then padded with additional bytes using the `pad()` function from the `Crypto.Util.Padding` module. The data is then encrypted using AES with a secret key and initialization vector specified by `AES_SECRET_KEY` and `AES_IV` respectively. Finally, the encrypted data is encoded in base64 format and returned as the encrypted response.
        
        This custom renderer can be used in DRF views by specifying the `renderer_classes` attribute and including the `CustomAesRenderer` as we will see next.
        
    * Next, navigate to views.py in the agents directory and add the following;
        
        ```python
        # agents/views.py
        
        from rest_framework.views import APIView
        from rest_framework.response import Response
        from rest_framework import status
        from .renderers import CustomAesRenderer # newly added
        from .serializers import AgentSerializer
        from .models import Agent
        
        
        class AgentListPlainView(APIView):
            def get(self, request):
                agents = Agent.objects.all()
                if agents:
                    serializer = AgentSerializer(agents, many=True)
                    data = serializer.data
                data = {
                    'status': 'success',
                    'code' : status.HTTP_200_OK,
                    'data': data
                    }
                return Response(data, status=status.HTTP_200_OK)
        
        
        # newly added
        class AgentListEncryptedView(APIView):
            renderer_classes = [CustomAesRenderer]
        
            def get(self, request):
                agents = Agent.objects.all()
                if agents:
                    serializer = AgentSerializer(agents, many=True)
                    data = serializer.data
                data = {
                    'status': 'success',
                    'code' : status.HTTP_200_OK,
                    'data': data
                    }
                return Response(data, status=status.HTTP_200_OK)
        ```
        
        The `AgentListEncryptedView` performs a similar function as `AgentListPlainView` except that the response is routed through the renderer class `CustomAesRenderer` which handles the encryption before returning the ciphertext to the client.
        
    * Add this endpoint to the URL routes by navigating to the urls.py file in the agents directory and add the below;
        
        ```python
        # agents/urls.py
        
        from django.urls import path
        from . import views
        
        urlpatterns = [
            path('agent-list-plain/', views.AgentListPlainView.as_view(), name='agent-list-plain'),
        
        # newly added below
            path('agent-list-encrypted/', views.AgentListEncryptedView.as_view(), name='agent-list-encrypted'),
        ```
        
    * Testing the endpoint using the URL [http://localhost:8000/agent-list-encrypted/](http://localhost:8000/agent-list-encrypted/) we get the below;


        ![Encrypted response](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j4xrzuouzbpvt1i89n4i.png)


- We have now completed the endpoint that returns the data as plaintext and one that returns the data as ciphertext, next, we will implement an endpoint that takes the ciphertext and decrypts it using our KEY and Initialization Variable(IV) by following the below steps;
    
    * Navigate to the views.py file in the agents directory and add the below code;
        
        ```python
        
        # agents/views.py
        
        
        from rest_framework.views import APIView
        from rest_framework.response import Response
        from rest_framework import status
        from .renderers import CustomAesRenderer
        from .serializers import AgentSerializer
        from .models import Agent
        
        
        # newly added below
        
        import base64
        from Crypto.Cipher import AES
        from Crypto.Util.Padding import unpad
        from .renderers import AES_SECRET_KEY, AES_IV
        import json
        
        class AgentListPlainView(APIView):
            def get(self, request):
                agents = Agent.objects.all()
                if agents:
                    serializer = AgentSerializer(agents, many=True)
                    data = serializer.data
                data = {
                    'status': 'success',
                    'code' : status.HTTP_200_OK,
                    'data': data
                    }
                return Response(data, status=status.HTTP_200_OK)
        
        
        class AgentListEncryptedView(APIView):
            renderer_classes = [CustomAesRenderer]
        
            def get(self, request):
                agents = Agent.objects.all()
                if agents:
                    serializer = AgentSerializer(agents, many=True)
                    data = serializer.data
                data = {
                    'status': 'success',
                    'code' : status.HTTP_200_OK,
                    'data': data
                    }
                return Response(data, status=status.HTTP_200_OK)
        
        
        
        # newly added below
        
        class DecryptAgentList(APIView):
            def post(self, request, *args, **kwargs):
                # Decode the request data from base64
                encrypted_data = request.data['ciphertext']
                enc = base64.b64decode(encrypted_data)
                cipher = AES.new(AES_SECRET_KEY, AES.MODE_CBC, AES_IV)
                try:
                    decrypted_data = unpad(cipher.decrypt(enc),16)
                    decrypted_data = json.loads(decrypted_data)
                    data = {
                        "data" : decrypted_data
                    }
                    return Response(data)
                except Exception as e:
                    return Response({"data": f"An error- {e}"})
        ```
        
        The `DecryptAgentList` class is a subclass of the DRF `APIView` class and defines a `post` method that handles HTTP POST requests.
        
        Inside the `post` method, the encrypted data is extracted from the request body in base64 format and then decoded back to binary format using `base64.b64decode()`. Next, an AES cipher object is created using the same secret key and initialization vector (IV) that were used to encrypt the data. The `unpad()` function from the `Crypto.Util.Padding` module is used to remove any padding that was added to the data during encryption. The decrypted data is then loaded from JSON format using the `json.loads()` method and returned in a structured response that includes the decrypted response as a value of a key called `data`.
        
        Finally, the plaintext response is returned using the DRF `Response` class if decryption is successful else an error message is returned.
        
    * Register the `DecryptAgentList` endpoint on the URL routes by adding the below code to the urls.py of the agents directory;
        
        ```python
        
        # agents/urls.py
        
        from django.urls import path
        from . import views
        
        urlpatterns = [
            path('agent-list-plain/', views.AgentListPlainView.as_view(), name='agent-list-plain'),
            path('agent-list-encrypted/', views.AgentListEncryptedView.as_view(), name='agent-list-encrypted'),
        
        
        # newly added
        
            path('decrypt-agent-list/', views.DecryptAgentList.as_view(), name='decrypt-agent-list')
        ]
        ```
        
    * Proceed to test this endpoint
        


        ![Decryption response](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k4y11n4peci3zgx85huq.png)


        

# Conclusion

We have successfully implemented AES encryption in Django Rest Framework to secure our API responses. We have discussed the importance of data encryption and the benefits it provides in securing sensitive data. By utilizing the AES encryption algorithm, we have ensured that our data is protected from unauthorized access, making our application more secure. With these techniques, we can confidently deploy our Django Rest Framework application, knowing that our data is protected from prying eyes.