Search code examples
pythonencryptionqr-codepycryptodome

Error in Decryption of QR Code using PyCryptodome


I am implementing a python package which can encrypt and decrypt contents of QR code. I made a module named rsa_module.py, which encrypts and decrypts messages, as follows:

from Crypto.PublicKey import RSA
from Crypto.Random import get_random_bytes
from Crypto.Cipher import AES, PKCS1_OAEP
import os

def generate_keys(secret_code):
    key = RSA.generate(2048)
    encrypted_key = key.exportKey(passphrase=secret_code, pkcs=8,
                                  protection="scryptAndAES128-CBC")

    output_directory=os.path.dirname(os.path.abspath(__file__))+'/Keys/'
    if(os.path.exists(output_directory)):
        # Save encrypted private key
        file_out = open(output_directory + "/rsa_private_key.pem", "wb")
        file_out.write(encrypted_key)
        # Save public key
        file_out = open(output_directory + "/rsa_public_key.pem", "wb")
        file_out.write(key.publickey().exportKey())
    else:
        os.mkdir(output_directory)
        # Save encrypted private key
        file_out = open(output_directory + "/rsa_private_key.pem", "wb")
        file_out.write(encrypted_key)
        # Save public key
        file_out = open(output_directory + "/rsa_public_key.pem", "wb")
        file_out.write(key.publickey().exportKey())

def encrypt(message):
    output_directory=os.path.dirname(os.path.abspath(__file__))+'/Keys/'

    with open('encrypted_data.txt', 'wb') as out_file:
        recipient_key = RSA.import_key(
        open(output_directory + '/rsa_public_key.pem').read())
        session_key = get_random_bytes(16)
        cipher_rsa = PKCS1_OAEP.new(recipient_key)
        out_file.write(cipher_rsa.encrypt(session_key))

        cipher_aes = AES.new(session_key, AES.MODE_EAX)
        encoded = message.encode("latin-1")
        data = encoded
        ciphertext, tag = cipher_aes.encrypt_and_digest(data)
        out_file.write(cipher_aes.nonce)

        out_file.write(tag)

        out_file.write(ciphertext)

    with open('encrypted_data.txt', 'rb') as fobj:
        output = [l for l in fobj.readlines()]
    os.remove('encrypted_data.txt')
    return output

def decrypt(encrypted_message, secret_code):
    code = secret_code
    output_directory=os.path.dirname(os.path.abspath(__file__))+'/Keys/'

    with open('encrypted_data.txt', 'wb') as temp_file:
        for item in (encrypted_message):
            temp_file.write(item)
    with open('encrypted_data.txt', 'rb') as fobj:
        private_key = RSA.import_key(
        open(output_directory + '/rsa_private_key.pem').read(),
        passphrase=code)
        enc_session_key, nonce, tag, ciphertext = [ fobj.read(x) 
        for x in (private_key.size_in_bytes(), 
        16, 16, -1) ]
        cipher_rsa = PKCS1_OAEP.new(private_key)
        session_key = cipher_rsa.decrypt(enc_session_key)
        cipher_aes = AES.new(session_key, AES.MODE_EAX, nonce)
        data = cipher_aes.decrypt_and_verify(ciphertext, tag)

    os.remove('encrypted_data.txt')
    return data.decode('utf8')

def main():
    generate_keys('secret one')
    encrypted = encrypt('blah blah blah blo')
    #encrypt_file('blah blah blah blo')
    print('Encryption Complete!')
    print('Decrypting message now....')
    print(encrypted)
    print(decrypt(encrypted, 'secret one'))
    #decrypt_file('secret one')

if __name__=='__main__': main()

If I run this script then the message is being encrypted and decrypted successfully. But when I use the same functions in another module, which decrypts the message from the QR code, I get an error. The name of this qr code decryption model is decrypt_qr.py, and the code for it as follows:

from qrtools import qrtools
from PIL import Image
import zbarlight
import os
from rsa_module import decrypt as de

def decrypt(file_name, password):
    keys_directory=os.path.dirname(os.path.abspath(__file__))+'/Keys/'
    private_key_path = keys_directory + '/rsa_private_key.pem'

    if(os.path.exists(private_key_path)):
        output_directory=os.path.dirname(os.path.abspath(__file__))+'/Output/'
        file_path = output_directory + file_name + '.PNG'
        with open(file_path, 'rb') as image_file:
            image = Image.open(image_file)
            image.load()
        codes = zbarlight.scan_codes('qrcode', image)
        decoded_result=codes[0].decode('utf8')
        print(codes[0].decode('utf8'))
        return de(decoded_result, password)
    else:
        print('No Public key available. Generate Public key and Private key first.')
        return None

def main():
    print(decrypt('my-qr','My secret'))

if __name__=='__main__': main()

If I run the decrypt_qr.py, I receive the following error:

Traceback (most recent call last):   
  File "decrypt_qr.py", line 28, in <module>
    if __name__=='__main__': main()   
  File "decrypt_qr.py", line 26, in main
    print(decrypt('my-qr','My secret'))   
  File "decrypt_qr.py", line 20, in decrypt
    return de(decoded_result, password)   
  File "/Users/king/Documents/pyWorkspace/Encrypted_QR_Code_Using_AES/rsa_module.py", line 93, in decrypt
    temp_file.write(item)
TypeError: a bytes-like object is required, not 'str'

But if I run the rsa_module.py with just the message being passed then it does decrypt properly. Can anyone suggest where I am going wrong?

The encryption module, named encrypt_qr.py, is as follows:

from generate_qr import make_qr_and_save
from rsa_module import encrypt as en
from rsa_module import generate_keys
import os

def encrypt(message, filename, password, size=3):
    generate_keys(password)
    keys_directory=os.path.dirname(os.path.abspath(__file__))+'/Keys/'
    public_key_path = keys_directory + '/rsa_public_key.pem'

    if(os.path.exists(public_key_path)):
        encrypted_message = en(message)
        print('\n')
        print(encrypted_message)
        make_qr_and_save(encrypted_message, filename, size)
    else:
        print('No Public key available. Generate Public key and Private key first.')
        return None

def main():
    encrypt('Buzz off!','my-qr','My secret')

if __name__=='__main__': main()

If I run the encrypt_qr.py script then the qr code is generated properly and contains encrypted byte stream, which is then used in the decryption script (decrypt_qr.py).


Solution

  • You are passing in text, decoded from UTF-8:

    decoded_result=codes[0].decode('utf8')
    # ...
    return de(decoded_result, password)   
    

    but your decrypt() function expects this to be bytes, because you opened the file in binary mode:

    def decrypt(encrypted_message, secret_code):
        # ...
        with open('encrypted_data.txt', 'wb') as temp_file:
            for item in (encrypted_message):
                temp_file.write(item)  
    

    Pass in a sequence of bytes instead, not a single str object:

    decoded_result = codes[0]
    # ...
    return de([decoded_result], password)   
    

    Note that (encrypted_message) in for item in (encrypted_message): is the same thing as encrypted_message without parentheses. The (...) are only serving to group the element, no tuple is created. If you were to pass in a single bytes object, you'd be iterating over a sequence of integers (representing the individual bytes in the object).

    Also, there is no need to write the data to a file on disk. You could use a io.BytesIO() object for an in-memory file, or just slice up the message in the right block sizes. Using a file on disk here actually makes the code more complicated than it is, and you hardcoded the /tmp path (different OS-es use different paths, the tempfile module can abstract this for you).