This shows you the differences between two versions of the page.
|
ic:labs:06 [2023/10/01 23:30] razvan.smadu [Laboratorul 06 - AES Byte at a Time ECB Decryption] |
ic:labs:06 [2023/10/09 23:22] (current) razvan.smadu |
||
|---|---|---|---|
| Line 5: | Line 5: | ||
| Prezentarea PowerPoint pentru acest laborator poate fi găsită [[https://drive.google.com/file/d/1YFkD5I6yNgVf1y9bhK4xdR2xZgDZ4Dd1/view?usp=sharing|aici]]. | Prezentarea PowerPoint pentru acest laborator poate fi găsită [[https://drive.google.com/file/d/1YFkD5I6yNgVf1y9bhK4xdR2xZgDZ4Dd1/view?usp=sharing|aici]]. | ||
| - | Puteți lucra acest laborator folosind și platforma Google Colab, accesând acest [[https://colab.research.google.com/drive/1RE4vQdzyOUpC-fuE9bTQUM4-tW4anc7A?usp=sharing|link]]. | + | Puteți lucra acest laborator folosind platforma Google Colab, accesând acest [[https://colab.research.google.com/github/ACS-IC-labs/IC-labs/blob/main/labs/lab06/lab6.ipynb|link]]. |
| + | <hidden> | ||
| <spoiler Click pentru a vedea utils.py> | <spoiler Click pentru a vedea utils.py> | ||
| <file python utils.py> | <file python utils.py> | ||
| Line 360: | Line 361: | ||
| <note tip> | <note tip> | ||
| - | Funcția **findBlockSize()** mărește numărul de caractere adăugate, mărind lungimea mesajului. | + | Funcția **find_block_size()** mărește numărul de caractere adăugate, mărind lungimea mesajului. |
| Cum putem asocia dimensiunea mesajului cu numărul de blocuri criptate? | Cum putem asocia dimensiunea mesajului cu numărul de blocuri criptate? | ||
| Line 418: | Line 419: | ||
| ==== Lab Code ==== | ==== Lab Code ==== | ||
| + | <spoiler Click pentru a vedea lab06.py> | ||
| <file python lab06.py> | <file python lab06.py> | ||
| - | from math import ceil | ||
| import base64 | import base64 | ||
| - | import os | + | from math import ceil |
| - | from random import randint | + | from typing import List, Tuple |
| - | from Crypto.Cipher import AES | + | |
| - | from utils import * | + | |
| - | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes | + | |
| from cryptography.hazmat.backends import default_backend | from cryptography.hazmat.backends import default_backend | ||
| + | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes | ||
| + | from utils import * | ||
| + | |||
| backend = default_backend() | backend = default_backend() | ||
| - | def split_bytes_in_blocks(x, block_size): | + | def split_bytes_in_blocks(x: bytes, block_size: int) -> List[bytes]: |
| - | nb_blocks = ceil(len(x)/block_size) | + | """Splits a byte string into a list of blocks of equal size. |
| - | return [x[block_size*i:block_size*(i+1)] for i in range(nb_blocks)] | + | |
| + | Args: | ||
| + | x (bytes): The byte string to split. | ||
| + | block_size (int): The size of each block in bytes. | ||
| - | def pkcs7_padding(message, block_size): | + | Returns: |
| + | List[bytes]: A list of byte strings, each of length block_size, | ||
| + | except for the last one which may be shorter. | ||
| + | """ | ||
| + | nb_blocks = ceil(len(x) / block_size) | ||
| + | return [x[block_size * i : block_size * (i + 1)] for i in range(nb_blocks)] | ||
| + | |||
| + | |||
| + | def pkcs7_padding(message: bytes, block_size: int) -> bytes: | ||
| + | """Applies PKCS#7 padding to a byte string. | ||
| + | |||
| + | Args: | ||
| + | message (bytes): The byte string to pad. | ||
| + | block_size (int): The size of the block in bytes. | ||
| + | |||
| + | Returns: | ||
| + | bytes: A byte string that is a multiple of block_size in length, | ||
| + | with padding bytes added at the end. The value of each padding | ||
| + | byte is equal to the number of padding bytes added. | ||
| + | """ | ||
| padding_length = block_size - (len(message) % block_size) | padding_length = block_size - (len(message) % block_size) | ||
| if padding_length == 0: | if padding_length == 0: | ||
| Line 443: | Line 466: | ||
| - | def pkcs7_strip(data): | + | def pkcs7_strip(data: bytes) -> bytes: |
| + | """Removes PKCS#7 padding from a byte string. | ||
| + | |||
| + | Args: | ||
| + | data (bytes): The byte string to strip. | ||
| + | |||
| + | Returns: | ||
| + | bytes: A byte string with the padding bytes removed from the end. | ||
| + | """ | ||
| padding_length = data[-1] | padding_length = data[-1] | ||
| - | return data[:- padding_length] | + | return data[:-padding_length] |
| - | def encrypt_aes_128_ecb(msg, key): | + | def encrypt_aes_128_ecb(plaintext: bytes, key: bytes) -> bytes: |
| - | padded_msg = pkcs7_padding(msg, block_size=16) | + | """Encrypts a byte string using AES-128 in ECB mode. |
| + | |||
| + | Args: | ||
| + | plaintext (bytes): The byte string to encrypt. It will be padded | ||
| + | using PKCS#7. | ||
| + | key (bytes): The encryption key. It must be 16 bytes in length. | ||
| + | |||
| + | Returns: | ||
| + | bytes: A byte string that is the encrypted version of plaintext. | ||
| + | """ | ||
| + | padded_msg = pkcs7_padding(plaintext, block_size=16) | ||
| cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend) | cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend) | ||
| encryptor = cipher.encryptor() | encryptor = cipher.encryptor() | ||
| Line 455: | Line 496: | ||
| - | def decrypt_aes_128_ecb(ctxt, key): | + | def decrypt_aes_128_ecb(ciphertext: bytes, key: bytes) -> bytes: |
| + | """Decrypts a byte string using AES-128 in ECB mode. | ||
| + | |||
| + | Args: | ||
| + | ciphertext (bytes): The byte string to decrypt. It must be a multiple of | ||
| + | 16 bytes in length. | ||
| + | key (bytes): The decryption key. It must be 16 bytes in length. | ||
| + | |||
| + | Returns: | ||
| + | bytes: A byte string that is the decrypted version of ciphertext. The | ||
| + | PKCS#7 padding will be removed. | ||
| + | """ | ||
| cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend) | cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend) | ||
| decryptor = cipher.decryptor() | decryptor = cipher.decryptor() | ||
| - | decrypted_data = decryptor.update(ctxt) + decryptor.finalize() | + | decrypted_data = decryptor.update(ciphertext) + decryptor.finalize() |
| message = pkcs7_strip(decrypted_data) | message = pkcs7_strip(decrypted_data) | ||
| return message | return message | ||
| - | # You are not suppose to see this | + | |
| class Oracle: | class Oracle: | ||
| - | def __init__(self): | + | """A class that simulates an encryption oracle using AES-128 in ECB mode. |
| - | self.key = 'Mambo NumberFive'.encode() | + | You are not suppose to see this""" |
| - | self.prefix = 'PREF'.encode() | + | |
| - | self.target = base64.b64decode( # You are suppose to break this | + | def __init__(self) -> None: |
| - | "RG8gbm90IGxheSB1cCBmb3IgeW91cnNlbHZlcyB0cmVhc3VyZXMgb24gZWFydGgsIHdoZXJlIG1vdGggYW5kIHJ1c3QgZGVzdHJveSBhbmQgd2hlcmUgdGhpZXZlcyBicmVhayBpbiBhbmQgc3RlYWwsCmJ1dCBsYXkgdXAgZm9yIHlvdXJzZWx2ZXMgdHJlYXN1cmVzIGluIGhlYXZlbiwgd2hlcmUgbmVpdGhlciBtb3RoIG5vciBydXN0IGRlc3Ryb3lzIGFuZCB3aGVyZSB0aGlldmVzIGRvIG5vdCBicmVhayBpbiBhbmQgc3RlYWwuCkZvciB3aGVyZSB5b3VyIHRyZWFzdXJlIGlzLCB0aGVyZSB5b3VyIGhlYXJ0IHdpbGwgYmUgYWxzby4=" | + | self.key = "Mambo NumberFive".encode() |
| + | self.prefix = "PREF".encode() | ||
| + | |||
| + | # You are suppose to break this | ||
| + | self.target = base64.b64decode( | ||
| + | "RG8gbm90IGxheSB1cCBmb3IgeW91cnNlbHZlcyB0cmVhc3VyZXMgb24gZWFydGgsI" | ||
| + | "HdoZXJlIG1vdGggYW5kIHJ1c3QgZGVzdHJveSBhbmQgd2hlcmUgdGhpZXZlcyBicm" | ||
| + | "VhayBpbiBhbmQgc3RlYWwsCmJ1dCBsYXkgdXAgZm9yIHlvdXJzZWx2ZXMgdHJlYXN" | ||
| + | "1cmVzIGluIGhlYXZlbiwgd2hlcmUgbmVpdGhlciBtb3RoIG5vciBydXN0IGRlc3Ry" | ||
| + | "b3lzIGFuZCB3aGVyZSB0aGlldmVzIGRvIG5vdCBicmVhayBpbiBhbmQgc3RlYWwuC" | ||
| + | "kZvciB3aGVyZSB5b3VyIHRyZWFzdXJlIGlzLCB0aGVyZSB5b3VyIGhlYXJ0IHdpbG" | ||
| + | "wgYmUgYWxzby4=" | ||
| ) | ) | ||
| - | def encrypt(self, message): | + | def encrypt(self, message: bytes) -> bytes: |
| return encrypt_aes_128_ecb( | return encrypt_aes_128_ecb( | ||
| self.prefix + message + self.target, | self.prefix + message + self.target, | ||
| - | self.key | + | self.key, |
| ) | ) | ||
| + | |||
| # Task 1 | # Task 1 | ||
| - | def findBlockSize(): | + | def find_block_size() -> Tuple[int, int, int]: |
| - | initialLength = len(Oracle().encrypt(b'')) | + | initial_length = len(Oracle().encrypt(b"")) |
| i = 0 | i = 0 | ||
| - | while 1: # Feed identical bytes of your-string to the function 1 at a time until you get the block length | + | |
| - | # You will also need to determine here the size of fixed prefix + target + pad | + | block_size = 0 |
| - | # And the minimum size of the plaintext to make a new block | + | size_of_prefix_target_padding = 0 |
| - | length = len(Oracle().encrypt(b'X'*i)) | + | minimum_size_to_align_plaintext = 0 |
| + | |||
| + | while 1: | ||
| + | # Feed identical bytes of your-string to the function 1 at a time | ||
| + | # until you get the block length. You will also need to determine | ||
| + | # here the size of fixed prefix + target + pad, and the minimum | ||
| + | # size of the plaintext to make a new block | ||
| + | length = len(Oracle().encrypt(b"X" * i)) | ||
| i += 1 | i += 1 | ||
| - | return block_size, sizeOfPrefixTargetPadding, minimumSizeToAlighPlaintext | + | # TODO 1: find block_size, size_of_prefix_target_padding, |
| + | # and minimum_size_to_align_plaintext | ||
| + | break | ||
| + | |||
| + | return ( | ||
| + | block_size, | ||
| + | size_of_prefix_target_padding, | ||
| + | minimum_size_to_align_plaintext, | ||
| + | ) | ||
| # Task 2 | # Task 2 | ||
| - | def findPrefixSize(block_size): | + | def find_prefix_size(block_size: int) -> int: |
| - | previous_blocks = None | + | initial_blocks = split_bytes_in_blocks(Oracle().encrypt(b""), block_size) |
| - | # Find the situation where prefix_size + padding_size - 1 = block_size | + | |
| - | # Use split_bytes_in_blocks to get blocks of size(block_size) | + | # TODO 2: Find when prefix_size + padding_size - 1 = block_size |
| + | # Use split_bytes_in_blocks to get blocks of size block_size. | ||
| + | |||
| + | # TODO 2.1: Find the block containing the prefix by comparing | ||
| + | # initial_blocks and modified_blocks | ||
| + | # You may find enumerate() and zip() useful. | ||
| + | modified_blocks = split_bytes_in_blocks(Oracle().encrypt(b"X"), block_size) | ||
| + | prefix_block_index = 0 | ||
| + | |||
| + | # TODO 2.2: As now we know in which block to look, find when that block | ||
| + | # does not change anymore when adding more X's. The complementary will | ||
| + | # represent the prefix. | ||
| + | prefix_size_in_block = 0 | ||
| + | prefix_size = prefix_block_index * block_size + prefix_size_in_block | ||
| return prefix_size | return prefix_size | ||
| # Task 3 | # Task 3 | ||
| - | def recoverOneByteAtATime(block_size, prefix_size, target_size): | + | def recover_one_byte_at_a_time( |
| + | block_size: int, | ||
| + | prefix_size: int, | ||
| + | target_size: int, | ||
| + | ) -> str: | ||
| known_target_bytes = b"" | known_target_bytes = b"" | ||
| + | |||
| for _ in range(target_size): | for _ in range(target_size): | ||
| # prefix_size + padding_length + known_len + 1 = 0 mod block_size | # prefix_size + padding_length + known_len + 1 = 0 mod block_size | ||
| known_len = len(known_target_bytes) | known_len = len(known_target_bytes) | ||
| - | padding_length = (- known_len - 1 - prefix_size) % block_size | + | padding_length = (-known_len - 1 - prefix_size) % block_size |
| padding = b"X" * padding_length | padding = b"X" * padding_length | ||
| - | # target block plaintext contains only known characters except its last character | + | # TODO 3.1: Determine the target block index which contains only known |
| - | # Don't forget to use split_bytes_in_blocks to get the correct block | + | # characters except its last character. |
| - | # trying every possibility for the last character | + | # TODO 3.2: Get the target block form split_bytes_in_blocks at the index |
| + | # previously determined. | ||
| - | print(known_target_bytes.decode()) | + | # TODO 3.3: Try every possibility for the last character and search for |
| + | # the block that you already know. That character will be added to | ||
| + | # the known target bytes. | ||
| + | return known_target_bytes.decode() | ||
| - | # Find block size, prefix size, and length of plaintext size to allign blocks | ||
| - | block_size, sizeOfPrefixTargetPadding, minimumSizeToAlignPlaintext = findBlockSize() | ||
| - | print("Block size:\t\t\t\t" + str(block_size)) | ||
| - | print("Size of prefix, target, and padding:\t" + str(sizeOfPrefixTargetPadding)) | ||
| - | print("Pad needed to align:\t\t\t" + str(minimumSizeToAlignPlaintext)) | ||
| - | # Find size of the prefix | + | def main() -> None: |
| - | prefix_size = findPrefixSize(block_size) | + | # Find block size, prefix size, and length of plaintext size to align blocks |
| - | print("\nPrefix Size:\t" + str(prefix_size)) | + | ( |
| + | block_size, | ||
| + | size_of_prefix_target_padding, | ||
| + | minimum_size_to_align_plaintext, | ||
| + | ) = find_block_size() | ||
| - | # Size of the target | + | print(f"Block size:\t\t\t\t{block_size}") |
| - | target_size = sizeOfPrefixTargetPadding - \ | + | print( |
| - | minimumSizeToAlignPlaintext - prefix_size | + | "Size of prefix, target, and padding:" |
| + | f"\t{size_of_prefix_target_padding}" | ||
| + | ) | ||
| + | print(f"Pad needed to align:\t\t\t{minimum_size_to_align_plaintext}") | ||
| - | print("\nTarget:") | + | # Find size of the prefix |
| - | recoverOneByteAtATime(block_size, prefix_size, target_size) | + | prefix_size = find_prefix_size(block_size) |
| + | print(f"\nPrefix Size:\t{prefix_size}") | ||
| + | |||
| + | # Size of the target | ||
| + | target_size = ( | ||
| + | size_of_prefix_target_padding | ||
| + | - minimum_size_to_align_plaintext | ||
| + | - prefix_size | ||
| + | ) | ||
| + | |||
| + | # Recover the target | ||
| + | recovered_target = recover_one_byte_at_a_time( | ||
| + | block_size, | ||
| + | prefix_size, | ||
| + | target_size, | ||
| + | ) | ||
| + | print(f"\nTarget: {recovered_target}") | ||
| + | |||
| + | |||
| + | if __name__ == "__main__": | ||
| + | main() | ||
| </file> | </file> | ||
| + | </spoiler> | ||
| + | </hidden> | ||