DEV Community

Cover image for Building a QR Code Identity System with Fernet Symmetric Encryption Algorithm in Python
LordGhostX
LordGhostX

Posted on • Edited on • Originally published at lordghostx.hashnode.dev

Building a QR Code Identity System with Fernet Symmetric Encryption Algorithm in Python

mymatric.png

A while ago, I made a tweet about implementing Fernet symmetric encryption algorithm, you can see the tweet here and the gist here

At the time, I was working on a web app that needed me to encode data that was to be sent via email. I didn't want to leave the data in plain text so I thought, why not encrypt it then decrypt when the user enters the data back into the app. I didn't want a situation where an attacker would be able to reverse engineer the system leading to a vulnerability hence the need for encryption.

The code in the tweet was extracted from a password manager called PVault which I wrote sometime last year, It used Fernet to encrypt stored passwords that could only be decrypted with a master key. You can find the repo here

What is Fernet Algorithm?

fernet.png
Image gotten from www.asecuritysite.com

Fernet is a symmetric encryption algorithm that makes sure that the message encrypted cannot be manipulated/read without the key. It uses URL safe encoding for the keys. Fernet also uses 128-bit AES in CBC mode and PKCS7 padding, with HMAC using SHA256 for authentication. The IV is created from os.random()

I know that's a lot to take in especially if you don't have any knowledge about cryptography and all so let me try and explain in simpler terms

Fernet algorithm is one that allows you to encrypt messages and decrypt them using a specific key (you can refer this to a password). Let's take a door, for example, the doors in your house can only be accessed by their key, and whatever lies behind the door is only available to whoever is in possession of the right key. So let us say the text "Hello World" is encrypted with the key "password" the resulting string could then be "Ifmmp-Xpsme" and you can only recover the original text with the key used in encrypting it. It's also like your online passwords. They can only be unlocked with the right key.

What are QR Codes?

blog.png

You've probably seen one of these around, those are QR Codes. It's a way of encoding data into black and white images like dots as shown above (custom colors can be set too). They're commonly used to store URL addresses just like the one above, If you scan that it leads you to this blog post, how cool is that :)

Identity Systems hmm...

The kind of identity system being referred to here is similar to ID cards and the rest which can be used to personally identify a person. In this blog post, we are going to be assuming we are a school issuing ID cards to students that contain a QR code which when scanned brings out the student's full profile as opposed to the traditional way of writing all the info on it.

Let's start writing code

Python3-powered_hello-world.svg.png
Image gotten from www.wikipedia.org

I'm sure you're already tired of all the talk and hungry for code, Well we're at that junction now. We are going to be using the Python language because It's what I'm most proficient in and It has libraries to help us complete our task easier.

Generating QR Codes

Before we proceed, you need to have pyqrcode, pypng and pyzbar all installed. Follow the links for their installation guide.

# import necessary libraries
import pyqrcode

# generate our QRCode
qr = pyqrcode.create("Hello World, This is my first QRCode!!!")
Enter fullscreen mode Exit fullscreen mode

You can also extend this with a few things

# export to png
qr.png("qr.png", scale=6)

# export as a custom color
qr.png("qr-colored.png", scale=6, module_color="#2962ff")

# export in text form
print(qr.text())

# print in the terminal
print(qr.terminal("blue", "white"))
Enter fullscreen mode Exit fullscreen mode

Reading QR codes

# importing necessary libraries
from PIL import Image
from pyzbar.pyzbar import decode

# reading the qr image
data = decode(Image.open("qr.png"))
text = data[0].data.decode("utf-8")
print(text)
Enter fullscreen mode Exit fullscreen mode

Moving forward

Now we have our QR generator and reader, what's next? Let's go back to our school ID card example, we are going to assume we have 5 students in the school and their data is represented below

matric_number name course level CGPA
e1vPNrA LordGhostX Python 100 5.0
vE3TIGU Yuno Magic 200 4.9
Ux3yLt0 Goku Karate 300 4.8
mAzIQN4 Tanjiro Music 400 4.7
uWyE4ns Gilgamesh Archery 500 4.6

To create our QR cards for the students, we need to encode the matric_number of each student into our cards, then when the QR is scanned to identify the student, the student database is then queried and the result is returned. Let's write semi-pseudo-code for this

# import necessary libraries
from PIL import Image
import pyqrcode
from pyzbar.pyzbar import decode

def generate_student_qr(matric_number):
    qr = pyqrcode.create(matric_number)
    qr.png(matric_number + ".png", scale=6)

def get_student_data(qr_image):
    data = decode(Image.open(qr_image))
    matric_number = data[0].data.decode("utf-8")
    # query DB for student info
    # SELECT * FROM students WHERE matric_number=matric_number
    student_data = "Student Data"
    if student_data == None:
        return "The student does not exist or the QR is invalid"
    return student_data

# register new student QR
matric_number = "test"
generate_student_qr(matric_number)

# get student data
student_data = get_student_data(matric_number + ".png")
print(student_data)
Enter fullscreen mode Exit fullscreen mode

With this, we've got a fully working ID generator and reader

The need for encryption

pgp-encryption-hero.png
Image gotten from www.varonis.com

As it is, the system simply encodes the matric number of the student onto the QR, If a student manages to write a custom QR reader he/she would understand the algorithm behind the ID generation and would be able to clone and duplicate any student ID card in the system and this is very dangerous. We would want to encrypt the data before encoding it on the QR so even if it is read, the user only sees gibberish and only the server understands the actual data and this is where Fernet comes in.

Fernet will encrypt the data in a form that only people who own the key are able to use the data and in this case, our server. First install cryptography module here

# import necessary libraries
import base64
from PIL import Image
import pyqrcode
from pyzbar.pyzbar import decode
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.fernet import Fernet

# generate Fernet encryption key
def generate_fernet_key(master_key, salt):
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA512(),
        length=32,
        salt=salt.encode(),
        iterations=100000,
        backend=default_backend()
    )
    key = base64.urlsafe_b64encode(kdf.derive(master_key.encode()))
    return key.decode("utf-8")

# encrypt a given text with our key
def encrypt_text(text, key):
    encryptor = Fernet(key)
    hash = encryptor.encrypt(text.encode())
    return hash.decode()

# decrypt a given text with our key
def decrypt_text(hash, key):
    decryptor = Fernet(key)
    text = decryptor.decrypt(hash.encode())
    return text.decode()

# generate student_qr with encryption
def generate_student_qr(matric_number, key):
    hashed_matric = encrypt_text(matric_number, key)
    qr = pyqrcode.create(hashed_matric)
    qr.png(matric_number + ".png", scale=6)

# get student data with encryption
def get_student_data(qr_image, key):
    data = decode(Image.open(qr_image))
    hashed_matric = data[0].data.decode("utf-8")
    matric_number = decrypt_text(hashed_matric, key)
    # query DB for student info
    # SELECT * FROM students WHERE matric_number=matric_number
    student_data = "Student Data"
    if student_data == None:
        return "The student does not exist or the QR is invalid"
    return student_data

# generate key for example
# you need to first set a master_key and salt
master_key = "server master key"
server_salt = "server salt"
server_fernet_key = generate_fernet_key(master_key, server_salt)

# generate a new student ID with encryption
matric_number = "test"
generate_student_qr(matric_number, server_fernet_key)

# get student data
student_data = get_student_data(matric_number + ".png", server_fernet_key)
print(student_data)
Enter fullscreen mode Exit fullscreen mode

Let's go over what we just did, First, we implemented the Fernet encryption algorithm in Python using the cryptography module. Then we rewrote our ID generation algorithm this time using encryption
Let's take an example of how the data would have been before and after encryption

Before encryption

  • matric_number = "my matric"
  • data_encoded_in_qr = "my matric"

After encryption

  • matric_number = "my matric"
  • master_key = "master key"
  • server_salt = "server_salt"
  • data_encoded_in_qr = "gAAAAABenoO8V-2Xx2ys9taCb_4u4Ao-uZ70MdwuxPbTPITzhLbdQ3bqoBlOQfOBDVoA6YWAvkylJEG6t6lpTK94MhBmwliXHQ=="

Conclusion

  • We learned about generating and reading QR Codes in Python
  • We learned about the Fernet encryption algorithm and implemented it in Python
  • We made a QR Code Identity System
  • And you got to read my first article on Hashnode :)

In the future, I might write a second part of this article to build a web app with Flask which will generate Fernet keys from master keys and Encrypt data into QR Codes.

What to do from here?

Go on with your regular day, remember to stay safe, and keep learning every day :)
psst... The QR code at the top of the article is the matric number we encrypted, scan it to reveal the value. And the QR code in the section about what QR codes is linked to this article when scanned.

Top comments (2)

Collapse
 
irby profile image
Matt Irby • Edited

Really cool post! I'm really interested in reviewing your GitHub code for the password manager. One edit suggestion is that the Fernet algorithm you've described is symmetric and not asymmetric.

Collapse
 
lordghostx profile image
LordGhostX

Thank you for this