Using Python for Encryption

Cryptography can be defined as the practice of hiding information and includes techniques for message-integrity checking, sender/receiver identity authentication, and digital signatures. The following are the four most common types of cryptography algorithms:

  • Hash functions: Also known as a one-way encryption, these have no key. A hash function outputs a fixed-length hash value for plaintext input, and in theory, it's impossible to recover the length or content of the plaintext. One-way cryptographic functions are used in websites to store passwords in a manner they cannot be retrieved
  • Keyed hash functions: Used to build message-authentication codes (MACs); MACs are intended to prevent brute-force attacks. So, they are intentionally designed to be slow.
  • Symmetric encryption: Output a ciphertext for some text input using a variable key, and you can decrypt the ciphertext using the same key. Algorithms that use the same key for both encryption and decryption are known as symmetric key algorithms.
  • Public key algorithms: For public key algorithms, there are two different keys: one for encryption and the other for decryption. Users of this technology publish their public keywhile keeping their private key secret. This enables anyone to send them a message encrypted with the public key, which only the holder of the private key can decrypt. These algorithms are designed to make the search for the private key extremely difficult, even if the corresponding public key is known to an attacker.

For example, for hash functions, Python provides some modules, such as hashlib. The following script returns the md5 checksum of the file. The code for this article is available at here.

You can find the following code in the md5.py file in the hashlib folder in the repository:

import hashlib

def md5Checksum(filePath):
   fh = open(filePath, 'rb')
   m = hashlib.md5()
   while True:
       data = fh.read(8192)
       if not data:
           break
       m.update(data)
   return m.hexdigest()

print('The MD5 checksum is', md5Checksum('md5.py'))

The output of this script will be as follows:

The MD5 checksum is 8eec2037fe92612b9a141a45b60bec26

Encrypting and decrypting information with pycrypto

When it comes to encrypting information with Python, one of the most reliable ones is the PyCrypto cryptographic library, which supports functions for block-encryption, flow-encryption, and hash-calculation.

The PyCrypto module provides all the necessary functions for implementing strong cryptography in a Python program, including both hash functions and encryption algorithms. For example, the block ciphers supported by pycrypto are:

  • AES
  • ARC2
  • Blowfish
  • CAST
  • DES
  • DES3
  • IDEA
  • RC5

In general, all these ciphers are used in the same way. You can use the Crypto.Cipher package to import a specific cipher type:

from Crypto.Cipher import [Cipher_Type]

You can use the new method constructor to initialize the cipher:

new ([key], [mode], [Vector IV])

With this method, only the key is mandatory, and you must take into account whether the type of encryption requires that it has a specific size. The possible modes are MODE_ECB, MODE_CBC, MODE_CFB, MODE_PGP, MODE_OFB, MODE_CTR, and MODE_OPENPGP.

If the MODE_CBC or MODE_CFB modes are used, the third parameter (Vector IV) must be initialized, which allows an initial value to be given to the cipher. Some ciphers may have optional parameters, such as AES, which can specify the block and key size with the block_size and key_size parameters.

In the same way as the hashlib, hash functions are also supported by pycrypto. The use of general hash functions with pycrypto is similar:

  • Use the Crypto.Hash package to import a specific hash type: from Crypto.Hash import [Hash Type]
  • Use the update method to set the datayouneedtoobtain the hash: update('data')
  • Use the hexdigest() method to generate the hash: hexdigest()

The following is the same example that you saw for obtaining the checksum of a file, in this case,you are using pycrypt instead of hashlib. You can find the following code in the hash.py file in the pycrypto folder:

from Crypto.Hash import MD5

def md5Checksum(filePath):
   fh = open(filePath, 'rb')
   m = MD5.new()
   while True:
       data = fh.read(8192)
       if not data:
           break
       m.update(data)
   return m.hexdigest()

print('The MD5 checksum is' + md5Checksum('hash.py'))

To encrypt and decrypt data, you can use the encrypt and decrypt functions:

encrypt ('clear text')
decrypt ('encrypted text')

Encrypting and decrypting with the DES algorithm

DES is a block cipher, which means that the text to be encrypted is a multiple of eight, so you added spaces at the end of the text. When you decipher it, you remove it.

The following script encrypts a user and a password and, finally, simulating that it is the server that has received these credentials, decrypts, and displays this data.

You can find the following code in the Encrypt_decrypt_DES.py file in the pycrypto folder:

from Crypto.Cipher import DES

# How we use DES, the blocks are 8 characters
# Fill with spaces the user until 8 characters
user =  "user    "
password = "password"

# we create the cipher with DES
cipher = DES.new('mycipher')

# encrypt username and password
cipher_user = cipher.encrypt(user)
cipher_password = cipher.encrypt(password)

# we send credentials
print("User: " + cipher_user)
print("Password: " + cipher_password)
# We simulate the server where the messages arrive encrypted.

# we decode messages and remove spaces with strip()
cipher = DES.new('mycipher')
decipher_user = cipher.decrypt(cipher_user).strip()
decipher_password = cipher.decrypt(cipher_password)
print("SERVER decipher:")
print("User: " + decipher_user)
print("Password: " + decipher_password)

The program encrypts the data using DES, so the first thing it does is import the DES module and create an encoder with the following instruction:

cipher = DES.new('mycipher')

The ‘mycipher’ parameter value is the encryption key. Once the cipher is created, encryption and decryption is quite simple.

Encrypting and decrypting with the AES algorithm

AES encryption needs a strong key. The stronger the key, the stronger your encryption. Your Initialization Vector needs to be 16 Bytes long. This will be generated using the random and string modules.

To use an encryption algorithm such as AES, you can import it from the Crypto.Cipher.AES package. As the PyCrypto block-level encryption API is very low level, it only accepts 16-, 24-, or 32-bytes-long keys for AES-128, AES-196, and AES-256, respectively.

Also, for AES encryption using pycrypto, you need to ensure that the data is a multiple of 16 bytes in length. Pad the buffer if it is not and include the size of the data at the beginning of the output, so the receiver can decrypt it properly.

You can find the following code in the Encrypt_decrypt_AES.py file in the pycrypto folder:

# AES pycrypto package
from Crypto.Cipher import AES

# key has to be 16, 24 or 32 bytes long
encrypt_AES = AES.new('secret-key-12345', AES.MODE_CBC, 'This is an IV-12')

# Fill with spaces the user until 32 characters
message = "This is the secret message      "

ciphertext = encrypt_AES.encrypt(message)
print("Cipher text: " , ciphertext)

# key must be identical
decrypt_AES = AES.new('secret-key-12345', AES.MODE_CBC, 'This is an IV-12')
message_decrypted = decrypt_AES.decrypt(ciphertext)

print("Decrypted text: ", message_decrypted.strip())

The output of the previous script will be as follows:

('Cipher text: ', '\xf2\xda\x92:\xc0\xb8\xd8PX\xc1\x07\xc2\xad"\xe4\x12\x16\x1e)(\xf4\xae\xdeW\xaf_\x9d\xbd\xf4\xc3\x87\xc4')('Decrypted text: ', 'This is the secret message')

File encryption with AES

AES encryption requires that each block being written be a multiple of 16 bytes in size. So, you read, encrypt, and write the data in chunks. The chunk size is required to be a multiple of 16. The following script encrypts the file provided by the parameter. You can find the following code in the aes-file-encrypt.py file in the pycrypto folder:

from Crypto.Cipher import AES
from Crypto.Hash import SHA256
import os, random, struct

def encrypt_file(key, filename):
   chunk_size = 64*1024
   output_filename = filename + '.encrypted'

   # Initialization vector
   iv = ''.join(chr(random.randint(0, 0xFF)) for i in range(16))

   #create the encryption cipher
   encryptor = AES.new(key, AES.MODE_CBC, iv)

   #Determine the size of the file
   filesize = os.path.getsize(filename)

   #Open the output file and write the size of the file.
   #We use the struct package for the purpose.
   with open(filename, 'rb') as inputfile:
       with open(output_filename, 'wb') as outputfile:
           outputfile.write(struct.pack('<Q', filesize))
           outputfile.write(iv)

           while True:
               chunk = inputfile.read(chunk_size)
               if len(chunk) == 0:
                   break
               elif len(chunk) % 16 != 0:
                   chunk += ' ' * (16 - len(chunk) % 16)
               outputfile.write(encryptor.encrypt(chunk))

password = "password"

def getKey(password):
   hasher = SHA256.new(password)
   return hasher.digest()

encrypt_file(getKey(password), 'file.txt');

The output of the previous script is a file called file.txt.encrypted, which contains the same content of the original file but the information is not legible. The previous script works in the way that first, you load all required modules and define the function to encrypt the file:

from Crypto.Cipher import AES
import os, random, struct
def encrypt_file(key, filename, chunk_size=64*1024):
output_filename = filename + '.encrypted'

Also, you need to obtain your Initialization Vector. A 16-byte initialization vector is required, which is generated as follows:

# Initialization vector
iv = ''.join(chr(random.randint(0, 0xFF)) for i in range(16))

Then, you can initialize the AES encryption method in the PyCrypto module:

encryptor = AES.new(key, AES.MODE_CBC, iv)
filesize = os.path.getsize(filename)

File decryption with AES

For decrypting, you need to reverse the preceding process using AES. You can find the following code in the aes-file-decrypt.py file in the pycrypto folder:

from Crypto.Cipher import AES
from Crypto.Hash import SHA256
import os, random, struct

def decrypt_file(key, filename):
   chunk_size = 64*1024
   output_filename = os.path.splitext(filename)[0]

   #open the encrypted file and read the file size and the initialization vector.
   #The IV is required for creating the cipher.
   with open(filename, 'rb') as infile:
       origsize = struct.unpack('<Q', infile.read(struct.calcsize('Q')))[0]
       iv = infile.read(16)

       #create the cipher using the key and the IV.
       decryptor = AES.new(key, AES.MODE_CBC, iv)

       #We also write the decrypted data to a verification file,
       #so we can check the results of the encryption
       #and decryption by comparing with the original file.
       with open(output_filename, 'wb') as outfile:
           while True:
               chunk = infile.read(chunk_size)
               if len(chunk) == 0:
                   break
               outfile.write(decryptor.decrypt(chunk))
           outfile.truncate(origsize)

password = "password"

def getKey(password):
   hasher = SHA256.new(password)
   return hasher.digest()

decrypt_file(getKey(password), 'file.txt.encrypted');

Encrypting and decrypting information with cryptography

Cryptography is a module more recent and it has better performance and security than pycrypto.

Introduction to cryptography

Cryptography is available in the pypi repository and you can install with the pip install cryptography command.

Cryptography includes both high-level and low-level interfaces to common cryptographic algorithms, such as symmetric ciphers, message digests, and key-derivation functions. For example, you can use symmetric encryption with the fernet package.

Symmetric encryption with the fernet package

Fernet is an implementation of symmetric encryption and guarantees that an encrypted message cannot be manipulated or read without the key. For generating the key, you can use the generate_key() method from the Fernet interface.

The following code is saved in the encrypt_decrypt.py file in the cryptography folder:

from cryptography.fernet import Fernet

key = Fernet.generate_key()
cipher_suite = Fernet(key)

print("Key "+str(cipher_suite))
message = "Secret message"

cipher_text = cipher_suite.encrypt(message)
plain_text = cipher_suite.decrypt(cipher_text)

print("\n\nCipher text: "+cipher_text)

print("\n\nPlain text: "+plain_text)

This is the output of the script: Python for Encryption 01

Using passwords with the fernet package

It is possible to use passwords with Fernet. To do this, you need to run the password through a key-derivation function, such as PBKDF2HMAC. PBKDF2 (Password Based Key Derivation Function 2) is typically used for deriving a cryptographic key from a password.

In this example, you’ll this function to generate a key from a password and use that key to create the Fernet object for encrypting and decrypting data. In this case, the data to encrypt is a simple message string. You can use the verify() method, which checks whether deriving a new key from the supplied key generates the same key as the expected key.

You can find the following code in the encrypt_decrypt_kdf.py file in the cryptography folder:

import base64
import os
from cryptography.fernet import Fernet
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

password = "password"
salt = os.urandom(16)
kdf = PBKDF2HMAC(algorithm=hashes.SHA256(),length=32,salt=salt,iterations=100000,backend=default_backend())

key = kdf.derive(password)

kdf = PBKDF2HMAC(algorithm=hashes.SHA256(),length=32,salt=salt,iterations=100000,backend=default_backend())

#verify() method checks whether deriving a new key from
#the supplied key generates the same key as the expected_key,
#and raises an exception if they do not match.
kdf.verify(password, key)

key = base64.urlsafe_b64encode(key)
fernet = Fernet(key)
token = fernet.encrypt("Secret message")

print("Token: "+token)
print("Message: "+fernet.decrypt(token))

This is the output of the script:

Python for Encryption 02

If you verify the key with the verify() method, and it checks that keys do not match during the process, it launches the cryptography.exceptions.InvalidKey exception:

Python for Encryption 03

Symmetric encryption with the ciphers package

The ciphers package from the cryptography module provides a class for symmetric encryption with the cryptography.hazmat.primitives.ciphers.Cipher class.

Cipher objects combine an algorithm, such as AES, with a mode, such as CBC or CTR. In the following script, you can see an example of encrypting and decrypting content with AES. You can find the code in the encrypt_decrypt_AES.py file in the cryptography folder:

import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend

backend = default_backend()
key = os.urandom(32)
iv = os.urandom(16)
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=backend)

encryptor = cipher.encryptor()
print(encryptor)

message_encrypted = encryptor.update("a secret message")

print("\n\nCipher text: "+message_encrypted)
ct = message_encrypted + encryptor.finalize()

decryptor = cipher.decryptor()

print("\n\nPlain text: "+decryptor.update(ct))

This is the output of the previous script:

Python for Encryption 04 If you found this article interesting, you can explore José Manuel Ortega’s Mastering Python for Networking and Security to build a network and perform security operations. Mastering Python for Networking and Security will help you get the most out of the Python language to build secure and robust networks that are resilient to attacks.

About the author

Óscar
has doubledaddy super powers, father of Hugo and Nico, husband of Marta, *nix user, Djangonaut and open source passionate.