Encryption and Decryption with Python Azure Functions

Thu Sep 30 2021 #Programming#Python#Azure

Inspired by Section 1.3 of David Kopec's "Classic Computer Science Problems in Python".

Source Code for this article on GitHub.

Let's build two Azure Functions, both HTTP triggers. One to encrypt a message, the other to decrypt it. Fair warning that if you are looking for a secure encryption solution, you would be better served in the hands of people smarter than me, but I think there is something useful to learn in working through this problem.

As noted above, the logic used in these examples comes from a book by David Kopec. I was working through it last night and wanted to take this section a little further. I reimplemented the work in Section 1.3 "Unbreakable Encryption" as two Azure Functions. I'll walk you through the process so you can too!

Gather Your Materials

Before you begin, you will need the following:

Create a Virtual Environment

Python Azure Functions App requires that you work within a virtual environment. If you are unfamiliar with using venv, there is a great primer over at RealPython.

Bash (MacOS or Linux)

python3.6 -m venv .env  
source .env/bin/activate

PowerShell or Command Prompt (Windows)

py -3.6 -m venv .env
.\.env\Scripts\activate

Initialize Your Azure Function App

Execute the following command to create a new Function App:

func init encryption_and_decryption

There is now a folder called encryption_and_decryption with the boilerplate code for a Python Azure Function App. If you would like, take some time to open the code in your favorite editor and have a look around.

If you are looking for an editor, I recommend VS Code.

Create the encrypt Function

Make sure you are in the encryption_and_decryption directory:

cd encryption_and_decryption

Create the new function using:

func new

At the prompt, choose HTTP trigger. Then name the function encrypt.

Encrypting Input

Open the file encrypt/__init__.py. You should see boilerplate code for an HTTP trigger function. This function will take name as a parameter and return "Hello name!". Let's add code at the bottom of this file that will instead encrypt the name using a random set of token bytes.

from secrets import token_bytes

def random_key(length: int) -> int:
    # returns length random bytes
    key: bytes = token_bytes(length)
    # convert key to bit string
    return int.from_bytes(key, "big")

Next, write a function to use this random key to encrypt name:

from typing import Tuple

def encrypt(name: str) -> Tuple[int, int]:
    # returns (encrypted_name, dummy_key), encrypted by one-time pad
    # encode the name to bytes, then to a bit string
    name_as_bytes: bytes = name.encode()
    name_key: int = int.from_bytes(name_as_bytes, "big")

    # generate a dummy key for encryption
    dummy_key: int = random_key(len(name_as_bytes))

    # encrypt by XOR operation
    encrypted_name: int = name_key ^ dummy_key
    
    return encrypted_name, dummy_key

Here, you have encoded the name to a number. This is just another way of writing the name data. The data can be translated back from this form. "Thomas", put though this transform is 92807522771315. You can see this in the Python REPL:

>>> name: str = "Thomas"
>>> name_as_bytes: bytes = name.encode()
>>> name_key = int.from_bytes(name_as_bytes, "big")
>>> print(f"name: {name}\nname_as_bytes: {name_as_bytes}\nname_key: {name_key}")
name: Thomas
name_as_bytes: b'Thomas'
name_key: 92807522771315

I can transform it back:

>>> name_again = name_key.to_bytes((name_key.bit_length() + 7) // 8, "big").decode()
>>> print(name_again)
Thomas

You take the int representation of the name, called name_key and perform an exclusive OR (XOR) operation with the random_key int. In Python XOR is done using the ^ character. This operation produces an encrypted form of the input name:

name_key ^ random_key = encrypted_name

Performing the XOR operation again with the random_key will undo the encryption:

encrypted_name ^ random_key = name_key

In a sense, the random_key locks and unlocks name.

Using encrypt

Now that you have the encrypt function written, edit the boilerplate code to take the name input, encrypt it and return the encrypted name with the key. Altogether, the code in __init__.py should like like this:

import logging
from secrets import token_bytes
from typing import Tuple


import azure.functions as func


def main(req: func.HttpRequest) -> func.HttpResponse:
    logging.info("func: encrypt: request received")

    name = req.params.get("name")
    if not name:
        try:
            req_body = req.get_json()
        except ValueError:
            pass
        else:
            name = req_body.get("name")

    if name:
        # encrypt name
        encrypted_name, dummy_key = encrypt(name)

        response = str({"encrypted_name": encrypted_name, "dummy_key": dummy_key})
        return func.HttpResponse(response)
    else:
        # ask for input if none provided
        return func.HttpResponse(
            "Please pass a name on the query string or in the request body",
            status_code=400,
        )


def random_key(length: int) -> int:
    # returns length random bytes
    key: bytes = token_bytes(length)
    # convert key to bit string
    return int.from_bytes(key, "big")


def encrypt(name: str) -> Tuple[int, int]:
    # return (encrypted_name, dummy_key); encrypted by one-time pad
    # encode the name to bytes, then to a bit string
    name_as_bytes: bytes = name.encode()
    name_key: int = int.from_bytes(name_as_bytes, "big")

    # generate a dummy key for encryption
    dummy_key: int = random_key(len(name_as_bytes))

    # encrypt by XOR operation
    encrypted_name: int = name_key ^ dummy_key

    return encrypted_name, dummy_key

Testing encrypt

Make sure you are in the encryption_and_decryption directory and run the Function App with the command:

func host start

The output of this command includes a link to the HTTP endpoint for this function: http://localhost:7071/api/encrypt.

Visiting this URL in a browser will call the Azure Function and you will get the response "Please pass a name on the query string or in the request body". Pass the Function a name parameter by appending ?name=Sophia to the URL so that it reads:

http://localhost:7071/api/encrypt?name=Sophia

You will get a response back that resembles JSON with the encrypted name and dummy key:

{
    "encrypted_name": 191667540651033,
    "dummy_key": 278440108655992
}

Try it out with different names!

Create the decrypt function

As you did with the encrypt function, from the command line, execute:

func new

At the prompt, choose HTTP trigger. Then name the function decrypt. You will pass both the encrypted_name and the dummy_key to this Azure Function. It will perform the XOR operation that undoes the encryption.

Decrypting Input

Open the file decrypt/__init__.py. At the bottom of this file, add a function to decrypt

def decrypt(encrypted_name: int, dummy_key: int) -> str:
    # returns XOR of encrypted_name and dummy_key converted to bytes and decoded
    name_key: int = encrypted_name ^ dummy_key
    return name_key.to_bytes((name_key.bit_length() + 7) // 8, "big").decode()

As shown above, performing the XOR operation on the encrypted_name and dummy_key will output the same value as name_key. Then, the function undoes the encoding from int to str.

Note: In Python 3+, the // operator divides numerator by denominator and returns the quotient as an int, dropping the digits after the decimal.

e.g. 5 // 2 == 2

Using decrypt

Edit the boilerplate code to accept two arguments encrypted_name and dummy_key. It should look like this:

import logging

import azure.functions as func


def main(req: func.HttpRequest) -> func.HttpResponse:
    logging.info("func: decrypt: request received")

    encrypted_name = req.params.get("encrypted_name")
    dummy_key = req.params.get("dummy_key")
    if not encrypted_name or not dummy_key:
        try:
            req_body = req.get_json()
        except ValueError:
            pass
        else:
            encrypted_name = req_body.get("encrypted_name")
            dummy_key = req_body.get("dummy_key")

    if encrypted_name and dummy_key:
        name = decrypt(int(encrypted_name), int(dummy_key))

        return func.HttpResponse(f"Hello {name}!")
    else:
        return func.HttpResponse(
            "Please pass a name on the query string or in the request body",
            status_code=400,
        )


def decrypt(encrypted_name: int, dummy_key: int) -> str:
    # returns XOR of encrypted_name and dummy_key converted to bytes and decoded
    name_key: int = encrypted_name ^ dummy_key
    return name_key.to_bytes((name_key.bit_length() + 7) // 8, "big").decode()

Testing decrypt

Start the Azure Function App with

func host start

Using the output from the encrypt Azure Function, test the decrypt Function by visiting:

http://localhost:7071/api/decrypt?encrypted_name=133673440856088&dummy_key=47262719615353

This should return "Hello Sophia!".

Try it with your own encrypted values by sending a name to the encrypt endpoint and replacing the values for encrypted_name and dummy_key with the values returned by the Azure Function.

Going Further

Python Azure Functions:

The encryption and decryption code comes from David Kopec's Classic Computer Science Problems in Python with the permission of the author.