Symmetric Cryptography with Python

Ashiq KS
6 min readJan 22, 2019

--

A Python article on the symmetric cryptography algorithms like AES, ChaCha20 with authentication and key derivation functions.

In this article, we will be implementing the symmetric cryptography like AES, ChaCha20, along with ‘Message Authentication Codes’ (MAC) in Python. We will see AES in two modes, Ciphertext Block Chaining (CBC) and Counter (CTR) modes with the authenticator ‘Poly1305’ MAC. Moreover, we will implement another popular symmetric algorithm used in cryptography, ChaCha20, again along with the Poly1305. I have written another article on hashing and MACs including Poly1305, so I highly recommend you to take a look at it before getting started on this article. This article doesn’t explain in detail any of the algorithms but gives implementation for these in Python.

Before Starting

We need a Python library called ‘PyCryptodome’ to be installed and it is imported as ‘Crypto’.

pip3 install pycryptodome

For symmetric cryptography, we need a shared key between the sender and the receiver. We will generate a secure key from a password using a key derivation function called ‘scrypt’ and will be using the key throughout the following algorithms. More on scrypt can be found on my another article.

All the codes used in this article is available in this Github repo.

import secrets
import scrypt
password = 'to be passed or guessed'
salt = secrets.token_bytes(32)
key = scrypt.hash(password, salt, N=2048, r=8, p=1, buflen=32)
print(key)
#prints b'J\x13w\x04#\xbe}\xebY\x9f/\xe3t\xbe\xbe\xf5\xd0?l\xf4\xca"\xb9G\xf2\xcc^\xfc\xe7\x1aP-'

We imported the scrypt and secrets libraries and generated a random secure salt to be given to the scrypt’s hash function along with our password to generate a random secure key of 32 bytes long.

Advanced Encryption Standard (AES)

AES is a fast and secure symmetric block cipher having a fixed data block size of 16 bytes and key can be 128, 192 or 256 bits long. It has many operational modes like CBC, CTR, Cipher FeedBack (CFB), Output FeedBack (OFB) and so on. In here, we will show the Python implementations of CBC and CTR modes.

Chaining Block Cipher (CBC)

CBC is a block mode and the methods in the AES expects the data to have a length multiple of 16 bytes. Let’s see it action and each part will be explained.

#Encryption and MAC generationfrom Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Hash import Poly_1305
def generate_Poly1305_mac(data, key):
mac = Poly1305.new(key=key, cipher=AES, data=data)
return (mac.hexdigest(), mac.nonce)
def verify_Poly1305_mac(data, key, nonce, mac_digest):
mac_verify = Poly1305.new(data=data, key=key, nonce=nonce,
cipher=AES)
try:
mac_verify.hexverify(mac_digest)
print('Message Authentication Success')
except:
print('Message Authentication Failed')
aes_enc = AES.new(key, AES.MODE_CBC)
data = b'message to be parsed using the AES symmetric cipher mode
with a key derived from scrypt key derivation function'
cipher_text = aes_enc.encrypt(pad(data, AES.block_size))
iv = aes_enc.iv
hexdigest, poly_nonce = generate_Poly1305_mac(data=data, key=key)
print('hexdigest: ', hexdigest)
print('poly_nonce: ', poly_nonce)
Output:
hexdigest: a9cdc78f38a7f44a33e741cfc706b92a
poly_nonce: b'\xb5\xfb3\xb9\xe8$\xaa \xdf`\x98\xa6\xae\xba\xc4\xf4'

We imported AES from ‘Crypto.Cipher’. Since this is a block mode, we have to pad the data to be encrypted so as to make it a multiple of 128-bits. For that, we import ‘pad’ and ‘unpad’ from ‘Crypto.Cipher’. ‘pad’ pads the data to make it ready for encryption and ‘unpad’ takes out the data from padding after decryption. The ‘pad()’ function has three arguments, the ‘data_to_pad’, ‘block_size’ in integers and ‘style’ — the type of padding, the default is ‘PKCS7’.

pad(data, AES.block_size) ‘AES.block_size’ gives the length of the block of operation, i.e 128-bits or 16 bytes. That is if the length of the data is not a multiple of 128-bits, then it gets padded to make it a multiple of 128-bits.

There are two custom functions — ‘generate_Poly1305_mac’ and ‘verify_Poly1305_mac’. The first function takes in the arguments as the data to be encrypted, and then the key. This function returns the hexadecimal digest of the MAC and the nonce value, which is used again for verification after decryption.

The ‘verify_Poly1305_mac’ takes in the data after decryption, the key and the hexadecimal digest sent along to verify whether the MAC of the received message is matching with the received MAC.

We now create an object of AES as follows.

aes_enc = AES.new(key, AES.MODE_CBC) It takes in the arguments as the key and the mode of operation. Here it is CBC, so we have to mention it as ‘AES.MODE_CBC’. It also takes in another argument — ‘iv’, initialization vector, which is a 16-byte value. If we don’t specify it generates a random ‘iv’ value. We have to keep this ‘iv’ value for the decryption process.

We get the ‘iv’ values by aes_enc.iv attribute.

cipher_text = aes_enc.encrypt(pad(data, AES.block_size))

This encrypts the padded data to a ciphertext saved in the variable ‘cipher_text’.

hexdigest, poly_nonce = generate_Poly1305_mac(data=data, key=key)

Now, we have the hexadecimal digest named ‘hexdigest’ and the nonce of Poly1305 MAC as ‘poly_nonce’ by applying the data and the key.

#Decryption and MAC verificationaes_dec = AES.new(key, AES.MODE_CBC, iv)
message = unpad(aes_dec.decrypt(cipher_text), AES.block_size)
verify_Poly1305_mac(data=message, key=key, nonce=poly_nonce, mac_digest=hexdigest)
print(message.decode('utf-8')) #prints message in the string format
Output:
Message Authentication Success
message: b'message to be parsed using the AES symmetric cipher mode with a key derived from scrypt key derivation function'

Now, we create a new object of AES with the same key, the CBC mode and the ‘iv’ value we got from the encryption process.

aes_dec = AES.new(key, AES.MODE_CBC, iv)

aes_dec.decrypt(cipher_text) decrypts the data + padding. After that, we apply the ‘unpad’ function to take the original message out of padding.

We next apply the ‘verify_Poly1305_mac’ to check the received MAC is equal to be generated MAC from the unpadded, decrypted message to make sure that the data has not been modified during transmission using the ‘hexverify’ function from ‘Poly1305’. The ‘hexverify’ function of the decryption object takes the hexadecimal digest received along for comparison.

verify_Poly1305_mac(data=message, key=key, nonce=poly_nonce, mac_digest=hexdigest)

If both the MACs are equal then we can be sure that the data has not been modified or tampered with. Later we print out the message in the string format by decoding it into the ‘utf-8’ format.

Counter Mode (CTR)

The CTR mode is different from the CBR mode in two ways: (1) the mode is represented as AES.MODE_CTR and (2) instead of ‘iv’ we use another term called ‘nonce’. Let’s see an example of CTR mode.

#Encryption and MAC calculationfrom Crypto.Cipher import AES
from Crypto.Hash import Poly1305
def generate_Poly1305_mac(data, key, cipher=AES):
mac = Poly1305.new(key=key, cipher=cipher, data=data)
return (mac.hexdigest(), mac.nonce)
def verify_Poly1305_mac(data, key, nonce, mac_digest, cipher=AES):
mac_verify = Poly1305.new(data=data, key=key, nonce=nonce,
cipher=AES)
try:
mac_verify.hexverify(mac_digest)
print('Message Authentication Success')
except:
print('Message Authentication Failed')
aes_enc = AES.new(key, AES.MODE_CTR)
data = b'message to be parsed using the AES symmetric cipher mode
with a key derived from scrypt key derivation function'
cipher_text = aes_enc.encrypt(data)
nonce = aes_enc.nonce
hexdigest, poly_nonce = generate_Poly1305_mac(data=data, key=key)
print('hexdigest: ', hexdigest)
print('poly_nonce: ', poly_nonce)
Output:hexdigest: 2ee72f9c899545b142cdd68d38962f6cpoly_nonce: b'\x8a\x11\xea+g\x03W\x91\x10\x9a\x97\xe1M\x94\xc5\xbc'

We get the nonce value using the ‘aes_enc.nonce’ attribute and it has to be used as the nonce for the decryption object.

#Decryption and MAC verificationaes_dec = AES.new(key, AES.MODE_CTR, nonce=nonce)
message = aes_dec.decrypt(cipher_text)
verify_Poly1305_mac(data=message, key=key, nonce=poly_nonce,
mac_digest=hexdigest)
print(message.decode('utf-8')) #Prints message in the string format
Output:Message Authentication Successmessage to be parsed using the AES symmetric cipher mode with a key derived from scrypt key derivation function

ChaCha20-Poly1305

It is an authenticated cipher, which means it is a symmetric cipher with authentication. The authentication is of Poly1305. That is we don’t have to implement a separate authentication as we did in the previous two cases. It itself generates a MAC at the encryption side and try to verify at the decryption side. The key size should be 32 bytes and nonce can be either 8 or 12 bytes long. Let’s see it in working.

#Encryptionfrom Crypto.Cipher import ChaCha20_Poly1305 as chadata = b'Cipher with authentication enabled'
chacha20 = cha.new(key=key)
cipher_text, mac = chacha20.encrypt_and_digest(data)
nonce = chacha20.nonce
print('cipher_text: ', cipher_text)
print('mac: ', mac)
#Output:cipher_text: b'\xcc\x95\xe0\xf20\x94\x85\x865K\xf4\xc5\xc4\x05\x08\x184\x10(\xbe\x15+\xebEw\x16\xd9\xa5\x079T\xd4\x92i'mac: b'y\x1fS\x86\xff3\xd9\x8bLq\x1b?m\xb5\xc7\x90'

The ‘ChaCha20_Poly1305’ is imported from ‘Crypto.Cipher’ as cha.

from Crypto.Cipher import ChaCha20_Poly1305 as cha

A new object of it is created as below.

chacha20 = cha.new(key=key)
cipher_text, mac = chacha20.encrypt_and_digest(data)

After creating the object we calculate the encrypted ciphertext and the MAC using the method ‘encrypt_and_digest()’ by taking in the data to be encrypted as the argument. It outputs a tuple, the first value is the ciphertext and the second value it MAC.

The nonce is kept for passing into the object for decryption. If we don’t specify a nonce value at the encryption side then the object itself generates a random nonce value.

#Decryptiontry:
chacha_poly = cha.new(key=key, nonce=nonce)
data = chacha_poly.decrypt_and_verify(cipher_text, mac)
print('The message is: ', data)
print('Message is verified')
except:
print("The message couldn't be verified")

For decryption, we instantiate a new object of ChaCha20_Poly1305 with the same key and the same nonce used in the encryption.

We will get the message text back and verify the authenticity of the message using the ‘decrypt_and_verify()’ method which takes in the cipher_text and the MAC generated at the encryption side.

That’s all for the symmetric encryption with AES and ChaCha20 in this article.

Please drop your comments and suggestion below.

--

--