Final 1
Final 1
Student iD : 21110053
Part 1
Trying to brute force the attack from both sides to find the intermediate ciphertext
def break_key():
ciphertext_hex = "4617AFD376CB9415"
plaintext_hex = "11111199AAB24567"
plaintext = hex_to_bin(plaintext_hex)
ciphertext = hex_to_bin(ciphertext_hex)
# Replace the ** in the strings with the current 2-byte values (kk1 and kk2)
string1 = 'AB13' + kk2 + '25F5231589'
string2 = 'F52315' + kk1 + 'AB132525'
string1= hex_to_bin(string1)
string2= hex_to_bin(string2)
try1 = encrypt(plaintext,string1)
try2 = decrypt(ciphertext,string2)
if try3 == try4:
print(f"Found successful keys: F52315{kk1}AB132525, AB13{kk}25F5231589")
return string2, string1
print("No matching keys found.")
return None, None
Pad the image dimensions to be divisible by 8 (AES block size is 16 bytes, but image dimensions
are typically processed in 8x8 blocks).
This is necessary because AES encryption works on byte data, not pixel arrays.
If not, the last partial block is padded and encrypted differently to ensure no data loss.
ECB Mode
Each 16-byte block is encrypted independently, meaning identical blocks in the image produce
identical ciphertext blocks.
CBC Mode
Each block is XORed with the previous ciphertext block before encryption, eliminating patterns.
def pad_image(img):
"""Pad image dimensions to be divisible by 8 using zeros"""
width, height = img.size
new_width = width + (8 - width % 8) if width % 8 != 0 else width
new_height = height + (8 - height % 8) if height % 8 != 0 else height
return img.resize((new_width, new_height))
# Convert to bytes
byte_data = img_array.tobytes()
return Image.fromarray(encrypted_array)
# Configuration
KEY = b'16bytesecretkey!'
IV = b'initialvector123'
# ECB Mode
plt.subplot(131)
ecb_img = process_image(AES.MODE_ECB, KEY)
plt.imshow(ecb_img)
plt.title('ECB Encrypted'), plt.axis('off')
# CBC Mode
plt.subplot(132)
cbc_img = process_image(AES.MODE_CBC, KEY, IV)
plt.imshow(cbc_img)
plt.title('CBC Encrypted'), plt.axis('off')
# Original Image
plt.subplot(133)
plt.imshow(Image.open('images.jpg'))
plt.title('Original Image'), plt.axis('off')
plt.tight_layout()
plt.show()
Part 3
import sys
import base64
import os
import hashlib
BLOCK_SIZE = 64
KEY_PERMUTATION_TABLE = [
56, 48, 40, 32, 24, 16, 8,
0, 57, 49, 41, 33, 25, 17,
9, 1, 58, 50, 42, 34, 26,
18, 10, 2, 59, 51, 43, 35,
62, 54, 46, 38, 30, 22, 14,
6, 61, 53, 45, 37, 29, 21,
13, 5, 60, 52, 44, 36, 28,
20, 12, 4, 27, 19, 11, 3
]
COMPRESSION_PERMUTATION_TABLE = [
13, 16, 10, 23, 0, 4,
2, 27, 14, 5, 20, 9,
22, 18, 11, 3, 25, 7,
15, 6, 26, 19, 12, 1,
40, 51, 30, 36, 46, 54,
29, 39, 50, 44, 32, 47,
43, 48, 38, 55, 33, 52,
45, 41, 49, 35, 28, 31
]
S_BOX_TABLE = [
[
[14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7],
[0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8],
[4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0],
[15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13]
],
[
[15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10],
[3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5],
[0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15],
[13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9]
],
[
[10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8],
[13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1],
[13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7],
[1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12]
],
[
[7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15],
[13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9],
[10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4],
[3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14]
],
[
[2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9],
[14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6],
[4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14],
[11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3]
],
[
[12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11],
[10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8],
[9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6],
[4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13]
],
[
[4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1],
[13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6],
[1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2],
[6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12]
],
[
[13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7],
[1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2],
[7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8],
[2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11]
]
]
EXPANSION_PERMUTATION_TABLE = [
31, 0, 1, 2, 3, 4,
3, 4, 5, 6, 7, 8,
7, 8, 9, 10, 11, 12,
11, 12, 13, 14, 15, 16,
15, 16, 17, 18, 19, 20,
19, 20, 21, 22, 23, 24,
23, 24, 25, 26, 27, 28,
27, 28, 29, 30, 31, 0
]
P_BOX_TABLE = [
15, 6, 19, 20, 28, 11, 27, 16,
0, 14, 22, 25, 4, 17, 30, 9,
1, 7, 23, 13, 31, 26, 2, 8,
18, 12, 29, 5, 21, 10, 3, 24
]
INITIAL_PERMUTATION_TABLE = [
57, 49, 41, 33, 25, 17, 9, 1,
59, 51, 43, 35, 27, 19, 11, 3,
61, 53, 45, 37, 29, 21, 13, 5,
63, 55, 47, 39, 31, 23, 15, 7,
56, 48, 40, 32, 24, 16, 8, 0,
58, 50, 42, 34, 26, 18, 10, 2,
60, 52, 44, 36, 28, 20, 12, 4,
62, 54, 46, 38, 30, 22, 14, 6
]
FINAL_PERMUTATION_TABLE = [
39, 7, 47, 15, 55, 23, 63, 31,
38, 6, 46, 14, 54, 22, 62, 30,
37, 5, 45, 13, 53, 21, 61, 29,
36, 4, 44, 12, 52, 20, 60, 28,
35, 3, 43, 11, 51, 19, 59, 27,
34, 2, 42, 10, 50, 18, 58, 26,
33, 1, 41, 9, 49, 17, 57, 25,
32, 0, 40, 8, 48, 16, 56, 24
]
def binary_to_message(binary_str):
"""Converts a binary string to text using UTF-8 encoding."""
# Split binary into 8-bit chunks
byte_chunks = [binary_str[i:i+8] for i in range(0, len(binary_str), 8)]
def binary_string_to_bytes(binary_str):
# Calculate padding needed to make binary_str length a multiple of 8
padded_length = ((len(binary_str) + 7) // 8) * 8
# Pad with leading zeros (left side) to make the length correct
padded_binary = binary_str.zfill(padded_length)
# Split into 8-bit chunks and convert to bytes
bytes_data = bytes([int(padded_binary[i:i+8], 2) for i in range(0, len(padded_binary), 8)])
return bytes_data
def pad_binary(binary_str):
block_size_bytes = 8
bytes_data = binary_string_to_bytes(binary_str)
padded_data = pad_bytes(bytes_data, block_size_bytes)
# Convert padded bytes back to binary string (each byte as 8 bits)
padded_binary = ''.join([format(byte, '08b') for byte in padded_data])
return padded_binary
def unpad_bytes(padded_data):
padding_length = padded_data[-1]
# Check for valid padding
if padding_length < 1 or padding_length > len(padded_data):
raise ValueError("Invalid padding")
if padded_data[-padding_length:] != bytes([padding_length] * padding_length):
raise ValueError("Invalid padding")
return padded_data[:-padding_length]
def unpad_binary(padded_binary_str):
# Split into 8-bit chunks and convert to bytes
byte_chunks = [padded_binary_str[i:i+8] for i in range(0, len(padded_binary_str), 8)]
bytes_data = bytes([int(chunk, 2) for chunk in byte_chunks])
# Remove padding
unpadded_data = unpad_bytes(bytes_data)
# Convert back to binary string (including leading zeros in bytes)
unpadded_binary = ''.join([format(byte, '08b') for byte in unpadded_data])
return unpadded_binary
def hex_to_bin(hex_str):
return f'{int(hex_str, 16):0{len(hex_str) * 4}b}'
def bin_to_hex(bin_string):
return hex(int(bin_string, 2))[2:].upper()
if len(encrypted_block) != block_size:
raise ValueError(f"Invalid encrypted block size: {len(encrypted_block)}")
encrypted_binary_str += encrypted_block
previous_block = encrypted_block # Update chain
return encrypted_binary_str
def read_image_bytes(filename="test.bmp"):
"""Read image file as binary string"""
with open(filename, 'rb') as f:
image_data = f.read()
return ''.join(f"{byte:08b}" for byte in image_data)
# Validate input
if len(encrypted_binary_str) % block_size != 0:
raise ValueError("Ciphertext length not multiple of block size")
decrypted_blocks = []
decrypted_blocks.append(plaintext_block)
previous_block = encrypted_block # Update chain
try:
byte_chunks = [decrypted_binary[i:i+8] for i in range(0, len(decrypted_binary), 8)]
bytes_data = bytes(int(chunk, 2) for chunk in byte_chunks)
unpadded_bytes = unpad_bytes(bytes_data)
return ''.join(f"{byte:08b}" for byte in unpadded_bytes)
except ValueError as e:
raise ValueError("Padding validation failed") from e
def gen_subkeys(key):
left_rotate_order = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1]
key_permutation = permute(key, KEY_PERMUTATION_TABLE)
fprint('KEY', key)
fprint('KEY PERMUTATION', key_permutation)
lk, rk = split_block(key_permutation)
subkeys = []
return subkeys
def split_block(block):
mid = len(block) // 2
return block[:mid], block[mid:]
def binary_to_message(binary_str):
# Split the binary string into 8-bit chunks
byte_strings = [binary_str[i:i+8] for i in range(0, len(binary_str), 8)]
return output
def receiver_decrypt_message(key, ciphertext):
# Initial permutation
initial_permutation = permute(ciphertext, INITIAL_PERMUTATION_TABLE)
fprint('INITIAL PERMUTATION', initial_permutation)
return final_permutation
# Generate subkeys
subkeys = gen_subkeys(key)
fprint('CIPHERTEXT', ciphertext)
return ciphertext
def read_message_from_file(filename="test.txt"):
"""Read text from file and convert to binary string"""
try:
with open(filename, 'r', encoding='utf-8') as f:
text = f.read()
return ''.join(f"{ord(c):08b}" for c in text)
except FileNotFoundError:
raise ValueError(f"File {filename} not found")
def read_encrypted_from_file(filename="encrypted.bin"):
"""Read encrypted bytes from file and convert to binary string"""
try:
with open(filename, 'rb') as f:
byte_data = f.read()
return ''.join(f"{byte:08b}" for byte in byte_data)
except FileNotFoundError:
raise ValueError(f"File {filename} not found")
def sender3(public,private2):
message = read_message_from_file()
print("\nBinary representation:")
print(binary_str)
hashed_message = create_sha1_hash(message)
print(f"SHA-1 hash of the message: {hashed_message}")
encrypted_hash=encrypt(private2,hashed_message)
print(f"the encrypted hash:{encrypted_hash}")
session_key = "F5231532AB132525"
session_key = hex_to_bin(session_key)
print("\nBinary representation:")
print(binary_str2)
encrypted_msg = encrypt_binary_string(session_key, binary_str)
encrypted_hashed_message = encrypt_binary_string(session_key, binary_str2)
print (f"binary hased message :{encrypted_hashed_message}")
session_key = "F5231532AB132525"
encrypted_ks=encrypt2(public,session_key)
write_encrypted_to_file(encrypted_msg)
return(encrypted_ks,encrypted_msg,encrypted_hashed_message)
def rec3(private,encrypted_ks,encrypted_msg,encrypted_hashed_message,public2):
session_key = decrypt(private,encrypted_ks)
session_key = session_key.hex()
session_key = hex_to_bin(session_key)
'''
quare_and_multiply for finding the multiplicative inverse of two numbers
'''
def square_and_multiply(base: int, exponent: int, modulus: int) -> int:
"""Modular exponentiation using square-and-multiply algorithm"""
result = 1
base = base % modulus # Ensure base is within modulus
while r != 0:
quotient = old_r // r
old_r, r = r, old_r - quotient * r
old_s, s = s, old_s - quotient * s
def is_prime(num):
if num == 2:
return True
if num < 2 or num % 2 == 0:
return False
for n in range(3, int(num**0.5)+2, 2):
if num % n == 0:
return False
return True
return decrypted_bytes
def pgp3():
p = int(input(" - Enter a prime number (17, 19, 23, etc): "))
q = int(input(" - Enter another prime number (Not one you entered above): "))
public, private = generate_key_pair(p, q)
print("now the second pair of keys")
public2, private2 = generate_key_pair(p, q)
encrypted_ks,encrypted_msg,encrypted_hashed_message=sender3(public,private2)
rec3(private,encrypted_ks,encrypted_msg,encrypted_hashed_message,public2)
def main():
pgp3()
if __name__ == "__main__":
main()
Part 4
The Square and Multiply algorithm is an efficient method for computing large modular
exponentiations, such as a^ b mod m. It reduces the number of multiplications required by leveraging
the binary representation of the exponent b. This is particularly useful in cryptography, where
exponents are very large (e.g., in RSA). Steps of the Algorithm: Convert the exponent to binary
Break down the exponent b into its binary digits. Iterate over each bit of the binary representation:
Square the current result. If the bit is 1, multiply the result by the base a. Apply modulo m at each
step to keep intermediate values manageable.
In code :
return result
My useage :
return decrypted_bytes.decode('utf-8')
Conclusion
The Square and Multiply algorithm is fundamental to RSA’s efficiency. By breaking modular
exponentiation into a series of squaring and multiplication steps, it enables secure
encryption/decryption even with massive numbers. The provided code uses this algorithm to
implement textbook RSA, though real-world systems would include additional safeguards (e.g.,
padding, optimized libraries)