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:
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.