3.17 VISUALIZING CIPHERTEXT CHANGES
EXERCISE 3.17: VISUALIZING CIPHERTEXT CHANGES
To better understand the difference between counter mode and cipher block chaining mode, go back to the image encryption utility you wrote previously. Modify it to first encrypt and then decrypt the image, using either AES-CBC or AES-CTR as the mode. After decryption, the original image should be completely restored.
Now introduce an error into the ciphertext and decrypt the modified bytes. Try, for example, picking the byte right in the middle of the encrypted image data and setting it to 0. After corrupting the data, call the decryption function and view the restored image. How much of a difference did the edit make with CTR? How much of a difference did the edit make with CBC?
HINT: If you can’t see anything, try an all-white image. If you still can’t see it, change 50 bytes or so to figure out where the changes are happening. Once you find where the changes are happening, go back to changing a single byte to view the differences between CTR and CBC. Can you explain what’s happening?
Remember the following top-secret.bmp
file from Exercise 3.11?
The following picture shows what we are going to do next.
The following code first encrypts top-secret.bmp
, introduces an error and then decrypts the corrupted ciphertext.
# ex3_17.py
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
import os
= 128 # bits
BLOCK_SIZE_OF_AES
= os.urandom(16) # we are using AES-128
key = os.urandom(16)
iv
def encryptUsingCBCAndThenCorruptIt():
= "top-secret.bmp"
ifile = "top-secret-cbc-encrypted.bmp"
ofile with open(ifile, "rb") as reader:
with open(ofile, "wb") as writer:
= reader.read()
image_data = image_data[:54], image_data[54:]
header, body
# Pad the body
= padding.PKCS7(BLOCK_SIZE_OF_AES).padder()
padder = padder.update(body)
body_padded += padder.finalize()
body_padded
# Encrypt the body
= Cipher(
encryptor =algorithms.AES(key),
algorithm=modes.CBC(iv),
mode=default_backend()
backend
).encryptor()= header + encryptor.update(body_padded)
ciphertext += encryptor.finalize()
ciphertext
# Corrupt the ciphertext
= corruptCipherText(ciphertext=ciphertext)
ciphertext
# Write the ciphertext to disk.
writer.write(ciphertext)
def decryptUsingCBC():
= "top-secret-cbc-encrypted.bmp"
ifile = "top-secret-cbc.bmp"
ofile with open(ifile, "rb") as reader:
with open(ofile, "wb") as writer:
= reader.read()
image_data = image_data[:54], image_data[54:]
header, body_encrypted
# Decrypt the body
= Cipher(
decryptor =algorithms.AES(key),
algorithm=modes.CBC(iv),
mode=default_backend()
backend
).decryptor()= decryptor.update(body_encrypted)
body_padded += decryptor.finalize()
body_padded
# Remove the padding bytes.
= padding.PKCS7(BLOCK_SIZE_OF_AES).unpadder()
unpadder = unpadder.update(body_padded)
body += unpadder.finalize()
body
# Write the plaintext to disk.
+ body)
writer.write(header
def encryptUsingCTRAndThenCorruptIt():
= "top-secret.bmp"
ifile = "top-secret-ctr-encrypted.bmp"
ofile with open(ifile, "rb") as reader:
with open(ofile, "wb") as writer:
= reader.read()
image_data = image_data[:54], image_data[54:]
header, body
# Encrypt the body
= Cipher(
encryptor =algorithms.AES(key),
algorithm=modes.CTR(iv),
mode=default_backend()
backend
).encryptor()= header + encryptor.update(body)
ciphertext += encryptor.finalize()
ciphertext
# Corrupt the ciphertext
= corruptCipherText(ciphertext=ciphertext)
ciphertext
# Write the ciphertext to disk.
writer.write(ciphertext)
def decryptUsingCTR():
= "top-secret-ctr-encrypted.bmp"
ifile = "top-secret-ctr.bmp"
ofile with open(ifile, "rb") as reader:
with open(ofile, "wb") as writer:
= reader.read()
image_data = image_data[:54], image_data[54:]
header, body
# Decrypt the body
= Cipher(
decryptor =algorithms.AES(key),
algorithm=modes.CTR(iv),
mode=default_backend()
backend
).decryptor()= header + decryptor.update(body)
plaintext += decryptor.finalize()
plaintext
# Write the plaintext to disk.
writer.write(plaintext)
def corruptCipherText(ciphertext: bytes)->bytes:
'''
This function changes the 54th byte of ciphertext to 0xff.
'''
= 54
k = ciphertext[:k] + b"\xff" + ciphertext[k+1:]
ciphertext return ciphertext
if __name__ == '__main__':
# CBC operation
encryptUsingCBCAndThenCorruptIt()
decryptUsingCBC()
# CTR operation
encryptUsingCTRAndThenCorruptIt() decryptUsingCTR()
So let’s run the above code:
top-secret-cbc.bmp
and top-secret-ctr.bmp
are shown below:
It is amazing how changing a single byte in the ciphertext, can have such dramatic color changes.
Note that: when you rerun the above code (i.e. ex3_17.py
) you might get different effects since the key
and iv
are generated randomly on each run.
Moral of the story: * Changing a single byte of a ciphertext that was encrypted with AES-CBC has the potential to change at most \(17\) bytes of the plaintext. (Why \(17\)? Think about it! 😉😉 Hint: Block size of AES is \(16\))
Changing a single byte of a ciphertext that was encrypted with AES-CTR has the potential to change at most \(1\) byte of the plaintext.
An enemy can fail to read a confidential message while still being able to change it in meaningful, deceptive ways.