How to use pycrypto, python-qrcode and Flask-RESTPlus to create QR codes that can send encrypted data to an endpoint

If you want to direct someone to a web page without saying a word, then you can use QR codes to do so.

For example, your QR code reader will direct your phone's browser to visit our home page when you scan the following QR Code:

Techcoil home page QRCode

Given that, you can use QR codes to send HTTP requests to an endpoint of your HTTP server. In addition, you can embed data that you wish to send to the endpoint in the QR codes.

With this in mind, let's look at how we can use pycrypto, python-qrcode and Flask-RESTPlus to create QR codes that can send encrypted data to an endpoint.

What happens when a QR code reader picks up an URL

When QR code readers pick up an URL in a QR code image, they start a web browser to retrieve the resource from the URL. Given that, the browser sends a HTTP GET request to that URL. In this situation, we can send data to the server through query string variables.

Given that, we will be able to embed some data in our QR code for a reader to send to a server endpoint. When we do so, we can fulfil functionalities to trigger a server action through a QR code.

Generating a QR Code image with python-qrcode

Previously, I discussed how we can create an API endpoint that generates a QR Code image, with Python 3 Flask-RESTPlus and python-qrcode.

When we run the sample script, we get an endpoint that receives HTTP POST requests made to /api/qrcode. If we post the following JSON model:

{
  "value": "https://www.techcoil.com"
}

then we will get the QR code image shown earlier in the HTTP response.

Given that, we can put the logic that returns a string value as a QR code image into a function:

from flask import send_file
from io import BytesIO
import qrcode

def qr_code_send_file(value_to_turn_into_qrcode):
    pil_img = qrcode.make(value_to_turn_into_qrcode)
    img_io = BytesIO()
    pil_img.save(img_io, 'PNG')
    img_io.seek(0)
    return send_file(img_io, mimetype='image/png')

Encrypting and decrypting data with pycrypto

Once we have the Python 3 codes to convert string values into QR code image, let's look at the encryption part.

But couldn't we pass the data as query string variables along with the URL as a string input to qr_code_send_file?

Although we can do so, we may want to encrypt the data before embedding it as an QR code for security reasons.

Given that, let's look at how we can encrypt and decrypt data in Python 3.

Previously, I discussed how we can encrypt and decrypt data in Python 3.

When you look at that post, you can find the following functions that we can use in this discussion:

from Crypto.Cipher import AES
 
import base64, json, math
 
# AES key must be either 16, 24, or 32 bytes long
COMMON_ENCRYPTION_KEY='asdjk@15r32r1234asdsaeqwe314SEFT'
# Make sure the initialization vector is 16 bytes
COMMON_16_BYTE_IV_FOR_AES='IVIVIVIVIVIVIVIV'
 
def get_common_cipher():
    return AES.new(COMMON_ENCRYPTION_KEY,
                   AES.MODE_CBC,
                   COMMON_16_BYTE_IV_FOR_AES)
 
def encrypt_with_common_cipher(cleartext):
    common_cipher = get_common_cipher()
    cleartext_length = len(cleartext)
    nearest_multiple_of_16 = 16 * math.ceil(cleartext_length/16)
    padded_cleartext = cleartext.rjust(nearest_multiple_of_16)
    raw_ciphertext = common_cipher.encrypt(padded_cleartext)
    return base64.b64encode(raw_ciphertext).decode('utf-8')
 
 
def decrypt_with_common_cipher(ciphertext):
    common_cipher = get_common_cipher()
    raw_ciphertext = base64.b64decode(ciphertext)
    decrypted_message_with_padding = common_cipher.decrypt(raw_ciphertext)
    return decrypted_message_with_padding.decode('utf-8').strip()
 
 
def encrypt_json_with_common_cipher(json_obj):
    json_string = json.dumps(json_obj)
    return encrypt_with_common_cipher(json_string)
 
 
def decrypt_json_with_common_cipher(json_ciphertext):
    json_string = decrypt_with_common_cipher(json_ciphertext)
    return json.loads(json_string)

When we have the above functions, we will be able to:

  • encrypt a JSON object into a cipher text string.
  • decrypt a cipher text string back into a JSON object.

Demonstrating that a QR code can send encrypted data to an endpoint which decrypts the data

In order to have visualize how to put the various parts together, let's build a demo app with two endpoints.

An endpoint that creates a QR Code representing a URL with a cipher text as a query string variable

First, let's create an endpoint that will take a JSON object and encrypt it into a cipher text. Once it had done so, it will include the cipher text as a query string variable in a URL:

from flask_restplus import Namespace, Resource, fields
import urllib.parse

# Create namespace for containing Qr Code related operations
qrcode_namespace = Namespace('QrCode', description='Qr code related operations')

# Define input model
qrcode_creation_input = qrcode_namespace.model('QRCode creation Input', {
    'id': fields.String(required=True, description='An Id'),
    'amount': fields.Integer(required=True, description='An amount')
})

@qrcode_namespace.route('/encrypted-payload')
class CreateInvoiceQrCode(Resource):

    @qrcode_namespace.expect(qrcode_creation_input)
    @qrcode_namespace.doc('Creates a QR code image that will bring the QR code reader to endpoint that will decrypt the contents.')
    @qrcode_namespace.produces(['image/png'])
    def post(self):

        thing_data_dict = request.get_json()
        cipher_text = encrypt_json_with_common_cipher(thing_data_dict)
        payload_decryption_url = '%s/api/decrypt/qrcode-details?ctext=%s' % (current_app.config['APP_URL'], urllib.parse.quote(cipher_text))

        return qr_code_send_file(payload_decryption_url)

An endpoint that decrypts the cipher text from a query string variable

After the QR code scans the QR Code image that was created by the endpoint earlier, it will hit the endpoint that decrypts the cipher text:

from json import JSONDecodeError

decrypt_namespace = Namespace('Decrypt', description='Decryption operations')

@decrypt_namespace.route('/qrcode-details')
class PayToQrCodeInvoice(Resource):

    @decrypt_namespace.doc('Url for QR code reader to reach in order to decrypt contents')
    def get(self):
        try:
            cipher_text = request.args.get('ctext')
            decrypted_json = decrypt_json_with_common_cipher(cipher_text)
            return decrypted_json, 200
        except JSONDecodeError as jde:
            print(jde)
            return 'Invalid cipher text provided', 400
        except TypeError as te:
            print(te)
            return 'Invalid cipher text provided', 400

Putting everything together

Given that we have explored the various code segments for the demo app, we can now build the following script:

#### Encryption / Decryption

from Crypto.Cipher import AES

import base64, json, math

# AES key must be either 16, 24, or 32 bytes long
COMMON_ENCRYPTION_KEY = 'asdjk@15r32r1234asdsaeqwe314SEFT'
# Make sure the initialization vector is 16 bytes
COMMON_16_BYTE_IV_FOR_AES = 'IVIVIVIVIVIVIVIV'

def get_common_cipher():
    return AES.new(COMMON_ENCRYPTION_KEY,
                   AES.MODE_CBC,
                   COMMON_16_BYTE_IV_FOR_AES)

def encrypt_with_common_cipher(cleartext):
    common_cipher = get_common_cipher()
    cleartext_length = len(cleartext)
    nearest_multiple_of_16 = 16 * math.ceil(cleartext_length / 16)
    padded_cleartext = cleartext.rjust(nearest_multiple_of_16)
    raw_ciphertext = common_cipher.encrypt(padded_cleartext)
    return base64.b64encode(raw_ciphertext).decode('utf-8')


def decrypt_with_common_cipher(ciphertext):
    common_cipher = get_common_cipher()
    raw_ciphertext = base64.b64decode(ciphertext)
    decrypted_message_with_padding = common_cipher.decrypt(raw_ciphertext)
    return decrypted_message_with_padding.decode('utf-8').strip()


def encrypt_json_with_common_cipher(json_obj):
    json_string = json.dumps(json_obj)
    return encrypt_with_common_cipher(json_string)


def decrypt_json_with_common_cipher(json_ciphertext):
    json_string = decrypt_with_common_cipher(json_ciphertext)
    return json.loads(json_string)

#### QrCode generation

from flask import send_file
from io import BytesIO
import qrcode


def qr_code_send_file(value_to_turn_into_qrcode):
    pil_img = qrcode.make(value_to_turn_into_qrcode)
    img_io = BytesIO()
    pil_img.save(img_io, 'PNG')
    img_io.seek(0)
    return send_file(img_io, mimetype='image/png')


#### Endpoints

from flask import Blueprint, current_app, Flask, request
from flask_restplus import Api

api_blueprint = Blueprint('API', __name__)

api = Api(api_blueprint,
    title='Encrypted payload within QR Code sample',
    version='1.0',
    description='API for demonstrating pass of encrypted payload from a QR code back to the server.'
    # All API metadatas
)

from flask_restplus import Namespace, Resource, fields
import urllib.parse

# Create namespace for containing Qr Code related operations
qrcode_namespace = Namespace('QrCode', description='Qr code related operations')

# Define input model
qrcode_creation_input = qrcode_namespace.model('QRCode creation Input', {
    'id': fields.String(required=True, description='An Id'),
    'amount': fields.Integer(required=True, description='An amount')
})

@qrcode_namespace.route('/encrypted-payload')
class CreateInvoiceQrCode(Resource):

    @qrcode_namespace.expect(qrcode_creation_input)
    @qrcode_namespace.doc('Creates a QR code image that will bring the QR code reader to endpoint that will decrypt the contents.')
    @qrcode_namespace.produces(['image/png'])
    def post(self):

        thing_data_dict = request.get_json()
        cipher_text = encrypt_json_with_common_cipher(thing_data_dict)
        payload_decryption_url = '%s/api/decrypt/qrcode-details?ctext=%s' % (current_app.config['APP_URL'], urllib.parse.quote(cipher_text))

        return qr_code_send_file(payload_decryption_url)

api.add_namespace(qrcode_namespace, path='/qrcode')

# Create namespace for decrypting payload

from json import JSONDecodeError

decrypt_namespace = Namespace('Decrypt', description='Decryption operations')

@decrypt_namespace.route('/qrcode-details')
class ShowDecryptedPayload(Resource):

    @decrypt_namespace.doc('Url for QR code reader to reach in order to decrypt contents')
    def get(self):
        try:
            cipher_text = request.args.get('ctext')
            decrypted_json = decrypt_json_with_common_cipher(cipher_text)
            return decrypted_json, 200
        except JSONDecodeError as jde:
            print(jde)
            return 'Invalid cipher text provided', 400
        except TypeError as te:
            print(te)
            return 'Invalid cipher text provided', 400


api.add_namespace(decrypt_namespace, path='/decrypt')

app = Flask(__name__)
# Change this to the URL where you host your app
app.config['APP_URL'] = 'https://qrcode.example.com'
app.register_blueprint(api_blueprint, url_prefix='/api')
app.run(host='0.0.0.0', port=12345)

Installing the dependencies

Before you can run the script, install the following dependencies into your Python 3 environment:

flask-restplus==0.12.1
pycrypto==2.6.1
Pillow
qrcode==6.1
Werkzeug==0.16.1

Running the demo script

Once you have installed the dependencies, change app.config['APP_URL'] to reflect the URL to reach your Python 3 application.

When you run the script, you will find the two endpoints:

  • /api/qrcode/encrypted-payload that handles HTTP POST requests.
  • /api/decrypt/qrcode-details that handles HTTP GET requests.

After you send a JSON object in a HTTP POST request to /api/qrcode/encrypted-payload, you will be able to get a QR code image in the HTTP response.

If you point your QR code reader at the image, you will then be redirected to /api/decrypt/qrcode-details which reveals the JSON object.

About Clivant

Clivant a.k.a Chai Heng enjoys composing software and building systems to serve people. He owns techcoil.com and hopes that whatever he had written and built so far had benefited people. All views expressed belongs to him and are not representative of the company that he works/worked for.