0% found this document useful (0 votes)
18 views19 pages

Final 1

The document outlines a cryptographic analysis performed by a student named Zaid Alfazza, detailing a meet-in-the-middle attack to find encryption keys and the process of encrypting an image using AES in both ECB and CBC modes. It includes code snippets for key breaking and image encryption, along with explanations of the steps taken and the advantages of using CBC mode over ECB. The document also emphasizes the importance of eliminating patterns in encrypted images to enhance security.

Uploaded by

zfazza4
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
18 views19 pages

Final 1

The document outlines a cryptographic analysis performed by a student named Zaid Alfazza, detailing a meet-in-the-middle attack to find encryption keys and the process of encrypting an image using AES in both ECB and CBC modes. It includes code snippets for key breaking and image encryption, along with explanations of the steps taken and the advantages of using CBC mode over ECB. The document also emphasizes the importance of eliminating patterns in encrypted images to enhance security.

Uploaded by

zfazza4
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 19

Crypto-HTU

Student name : Zaid alfazza

Student iD : 21110053

Part 1

1) The detailed step-by-step procedure to find the keys.

Meet in the middle attack

Trying to brute force the attack from both sides to find the intermediate ciphertext

1. Convert the plaintext and ciphertext into binary.


2. Loop through all possible values for kk1 and kk2 (65,536 iterations).
3. Generate two possible encryption keys.
4. Encrypt the plaintext with K1 and decrypt the ciphertext with K2.
5. If their results match, the keys are found.
6. If not, try the reverse order of encryption and decryption.
7. If still no match, return no keys found.

This meet-in-the-middle approach significantly reduces the complexity compared to


brute-forcing the full keyspace.

2) The values of the keys K1 and K2

After the test here is the key pair


AB13f625F5231589 F5231532AB132525
AB13f625F5231589 F5231532AB132525
AB13f625F5231589 F5231532AB132525
3) Your code/scripts (in text format).

def break_key():
ciphertext_hex = "4617AFD376CB9415"
plaintext_hex = "11111199AAB24567"
plaintext = hex_to_bin(plaintext_hex)
ciphertext = hex_to_bin(ciphertext_hex)

# Iterate over all possible 2-byte combinations for both keys


for i in range(256): # Outer loop for the first byte of the first key
for j in range(256): # Inner loop for the second byte of the first key
kk1 = format(i, '02x') # First 2-byte value
kk2 = format(j, '02x') # Second 2-byte value

# 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 try1 == try2: # Compare the results


print(f"Found successful keys: AB13{kk1}25F5231589, F52315{kk2}AB132525")
return string1, string2
else:
try3 = encrypt(plaintext, string2)
try4 = decrypt(ciphertext, string1)

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

4) Screenshots of the result of running the codes.


Part 2

1) The detailed step-by-step you followed in your test.

Step 1: Load and Prepare the Image


Open the image (images.jpg) and convert it to RGB mode to ensure consistency.

Pad the image dimensions to be divisible by 8 (AES block size is 16 bytes, but image dimensions
are typically processed in 8x8 blocks).

Convert the padded image to a NumPy array for easy manipulation.


Step 2: Convert Image Data to Bytes
The image array is flattened and converted into a byte stream.

This is necessary because AES encryption works on byte data, not pixel arrays.

Step 3: Encrypt the Image Using AES


AES key (16bytesecretkey!) and IV (initialvector123) are defined.

The encryption process uses Ciphertext Stealing (CTS):

If the data is a multiple of the AES block size, it is encrypted normally.

If not, the last partial block is padded and encrypted differently to ensure no data loss.

ECB Mode

Electronic Codebook (ECB) mode is used first.

Each 16-byte block is encrypted independently, meaning identical blocks in the image produce
identical ciphertext blocks.

This reveals patterns, making the encrypted image recognizable.

CBC Mode

Cipher Block Chaining (CBC) mode is used next.

An IV (Initialization Vector) ensures that identical blocks encrypt to different values.

Each block is XORed with the previous ciphertext block before encryption, eliminating patterns.

The first block is XORed with the IV.

Step 4: Convert Encrypted Data Back to an Image


The encrypted bytes are converted back into a NumPy array.

The reshaped array is transformed into an encrypted image.

Step 5: Display Results


Original image is displayed for reference.
ECB-encrypted image shows noticeable patterns due to its independent block encryption.

CBC-encrypted image appears visually randomized, proving its effectiveness in obscuring


patterns.

2) An explanation of why you found a specific mode more


suitable.

CBC mode is more suitable for encryption because it ensures that


identical plaintext blocks produce different ciphertext blocks,
effectively eliminating patterns. This is particularly important in
encrypting images, where ECB mode fails by preserving structural
patterns, making the encrypted image recognizable. By introducing
an initialization vector (IV) and chaining each block with the
previous ciphertext, CBC ensures diffusion, making it much harder
to analyze or reconstruct the original data from the ciphertext.

3) Include both output images with proper captions.


4) Your code/scripts (in text format)

Code :from PIL import Image


import numpy as np
from Crypto.Cipher import AES
import matplotlib.pyplot as plt

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))

def encrypt_cts(cipher, data):


"""Ciphertext Stealing (CTS) implementation"""
block_size = AES.block_size
if len(data) % block_size == 0:
return cipher.encrypt(data)

# Encrypt all blocks except last partial block


ciphertext = cipher.encrypt(data[:-block_size])

# Steal ciphertext for final block


temp = cipher.encrypt(data[-block_size:] + bytes(block_size))
return ciphertext + temp

def process_image(mode, key, iv=None):


# Read and pad image
img = Image.open('images.jpg').convert('RGB')
padded_img = pad_image(img)
img_array = np.array(padded_img)

# Convert to bytes
byte_data = img_array.tobytes()

# Encrypt with CTS


encrypt_cipher = AES.new(key, mode, iv=iv) if iv else AES.new(key, mode)
encrypted = encrypt_cts(encrypt_cipher, byte_data)
# Convert encrypted bytes to image array
encrypted_array = np.frombuffer(encrypted, dtype=np.uint8)
encrypted_array = encrypted_array[:np.prod(img_array.shape)].reshape(img_array.shape)

return Image.fromarray(encrypted_array)

# Configuration
KEY = b'16bytesecretkey!'
IV = b'initialvector123'

# Encrypt and display


plt.figure(figsize=(15, 5))

# 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

1) Your code/scripts (in text format).

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)]

# Convert binary chunks to bytes


try:
bytes_data = bytes(int(chunk, 2) for chunk in byte_chunks)
except ValueError:
raise ValueError("Invalid binary string - contains non-binary characters")

# Convert bytes to text with UTF-8 decoding


try:
return bytes_data.decode('utf-8')
except UnicodeDecodeError:
raise ValueError("Decoded bytes are not valid UTF-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_bytes(data, block_size):


padding_length = block_size - (len(data) % block_size)
if padding_length == 0:
padding_length = block_size # Ensure padding is added if already aligned
padding = bytes([padding_length] * padding_length)
return data + padding

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 xor2(a_binstr, b_binstr):


"""XOR two binary strings of equal length"""
return ''.join(str(int(a) ^ int(b)) for a, b in zip(a_binstr, b_binstr))

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()

def encrypt_binary_string(session_key, binary_str):


"""CBC encryption with fixed IV of all ones"""
block_size = 64
encrypted_binary_str = ""

# Fixed IV (64 bits of '1's)


iv = '1' * block_size
previous_block = iv

# Pad and process blocks


padded_binary = pad_binary(binary_str)

for i in range(0, len(padded_binary), block_size):


block = padded_binary[i:i+block_size]

# CBC XOR with previous ciphertext/IV


xored_block = xor2(block, previous_block)
encrypted_block = sender_encrypt_message(session_key, xored_block)

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)

def write_encrypted_image(encrypted_binary, filename="encrypted.bmp"):


"""Save encrypted binary string as image file"""
byte_data = bytes(int(encrypted_binary[i:i+8], 2)
for i in range(0, len(encrypted_binary), 8))
with open(filename, 'wb') as f:
f.write(byte_data)

def decrypt_binary_string(session_key, encrypted_binary_str):


"""CBC decryption with fixed IV of all ones"""
block_size = 64

# Fixed IV (same as encryption)


iv = '1' * block_size
previous_block = iv

# Validate input
if len(encrypted_binary_str) % block_size != 0:
raise ValueError("Ciphertext length not multiple of block size")

decrypted_blocks = []

# Process each block


for i in range(0, len(encrypted_binary_str), block_size):
encrypted_block = encrypted_binary_str[i:i+block_size]
# Decrypt and XOR with previous block
decrypted_xored = receiver_decrypt_message(session_key, encrypted_block)
plaintext_block = xor2(decrypted_xored, previous_block)

decrypted_blocks.append(plaintext_block)
previous_block = encrypted_block # Update chain

# Combine and unpad


decrypted_binary = ''.join(decrypted_blocks)

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 left_rotate(blocks, n_shifts):


return [block[n_shifts:] + block[:n_shifts] for block in blocks]

def permute(block, table):


return ''.join(block[i] for i in table)

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 = []

for n_shifts in left_rotate_order:


lk, rk = left_rotate([lk, rk], n_shifts)
compression_permutation = permute(lk + rk, COMPRESSION_PERMUTATION_TABLE)
subkeys.append(compression_permutation)

return subkeys

def fprint(text, value):


print(f'{text:>22}: {value}')

def split_block(block):
mid = len(block) // 2
return block[:mid], block[mid:]

def xor(block_1, block_2):


return f'{int(block_1, 2) ^ int(block_2, 2):0{len(block_1)}b}'
def s_box(block):
output = ''
for i in range(8):
sub_str = block[i * 6:i * 6 + 6]
row = int(sub_str[0] + sub_str[-1], 2)
column = int(sub_str[1:5], 2)
output += f'{S_BOX_TABLE[i][row][column]:04b}'
return output

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)]

# Convert each 8-bit string to a byte


bytes_data = bytes([int(b, 2) for b in byte_strings])

# Decode bytes back to UTF-8 string


return bytes_data.decode('utf-8')

def round(input_block, subkey):


l, r = split_block(input_block)
expansion_permutation = permute(r, EXPANSION_PERMUTATION_TABLE)
xor_with_subkey = xor(expansion_permutation, subkey)
s_box_output = s_box(xor_with_subkey)
p_box_output = permute(s_box_output, P_BOX_TABLE)
xor_with_left = xor(p_box_output, l)
output = r + xor_with_left

fprint('INPUT', f'{l} {r}')


fprint('SUBKEY', subkey)
fprint('EXPANSION PERMUTATION', expansion_permutation)
fprint('XOR', xor_with_subkey)
fprint('S-BOX SUBSTITUTION', s_box_output)
fprint('P-BOX PERMUTATION', p_box_output)
fprint('XOR', xor_with_left)
fprint('SWAP', f'{r} {xor_with_left}')
fprint('OUTPUT', output)

return output
def receiver_decrypt_message(key, ciphertext):
# Initial permutation
initial_permutation = permute(ciphertext, INITIAL_PERMUTATION_TABLE)
fprint('INITIAL PERMUTATION', initial_permutation)

# Split into left and right blocks


l_block, r_block = split_block(initial_permutation)

# Generate subkeys (same as in encryption, but reversed order)


subkeys = gen_subkeys(key)[::-1] # Reverse the subkey order for decryption

# Perform 16 rounds of Feistel function (in reverse order)


for round_no, subkey in enumerate(subkeys, start=1):
expanded_r_block = permute(r_block, EXPANSION_PERMUTATION_TABLE)
xor_result = xor(expanded_r_block, subkey)
substituted = s_box(xor_result)
permuted = permute(substituted, P_BOX_TABLE)
new_r_block = xor(l_block, permuted)
l_block = r_block
r_block = new_r_block

# Combine the blocks (r_block + l_block) and apply final permutation


combined_block = r_block + l_block
final_permutation = permute(combined_block, FINAL_PERMUTATION_TABLE)
fprint('FINAL PERMUTATION', final_permutation)

return final_permutation

def sender_encrypt_message(key, plaintext):


# Initial permutation
initial_permutation = permute(plaintext, INITIAL_PERMUTATION_TABLE)
fprint('INITIAL PERMUTATION', initial_permutation)

# Split into left and right blocks


l_block, r_block = split_block(initial_permutation)

# Generate subkeys
subkeys = gen_subkeys(key)

# Perform 16 rounds of Feistel function


for round_no, subkey in enumerate(subkeys, start=1):
expanded_r_block = permute(r_block, EXPANSION_PERMUTATION_TABLE)
xor_result = xor(expanded_r_block, subkey)
substituted = s_box(xor_result)
permuted = permute(substituted, P_BOX_TABLE)
new_r_block = xor(l_block, permuted)
l_block = r_block
r_block = new_r_block

# Combine the blocks (r_block + l_block) and apply final permutation


combined_block = r_block + l_block
ciphertext = permute(combined_block, FINAL_PERMUTATION_TABLE)

fprint('CIPHERTEXT', ciphertext)
return ciphertext

def compare_two_hashes(hash1: str, hash2: str) -> bool:


return hash1 == hash2

def create_sha1_hash(message: str) -> str:


# Create a new sha1 hash object
sha1 = hashlib.sha1()
# Update the hash object with the bytes of the message
sha1.update(message.encode('utf-8'))
# Get the hexadecimal representation of the digest
return sha1.hexdigest()

def bytes_to_plain_text(byte_array: bytes) -> str:


try:
# Decode the byte array to a plain text string using UTF-8 encoding
return byte_array.decode('utf-8')
except UnicodeDecodeError as e:
# Handle the case where the byte array cannot be decoded
print(f"Error decoding bytes: {e}")
return ""

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 write_encrypted_to_file(encrypted_binary, filename="encrypted.bin"):


"""Write encrypted binary string to file as bytes"""
# Convert binary string to bytes
byte_data = bytes(int(encrypted_binary[i:i+8], 2)
for i in range(0, len(encrypted_binary), 8))
with open(filename, 'wb') as f:
f.write(byte_data)

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 write_decrypted_to_file(decrypted_binary, filename="decrypted.txt"):


"""Convert decrypted binary to text and write to file"""
text = binary_to_message(decrypted_binary)
with open(filename, 'w', encoding='utf-8') as f:
f.write(text)

def sender3(public,private2):
message = read_message_from_file()

# Convert message to binary string (UTF-8 encoded bytes)


binary_str = ''.join([format(byte, '08b') for byte in message.encode('utf-8')])

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)

# Convert message to binary string (UTF-8 encoded bytes)


binary_str2 = ''.join([format(byte, '08b') for byte in encrypted_hash.encode('utf-8')])

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)

decrypted_hashed_msg = decrypt_binary_string(session_key, encrypted_hashed_message)


print(f"decrypted hased message {decrypted_hashed_msg}")
decrypted_msg= decrypt_binary_string(session_key, encrypted_msg)
decrypted_msg= binary_to_message(decrypted_msg)
decrypted_hashed_msg= binary_to_message(decrypted_hashed_msg)
hashed_message = create_sha1_hash(decrypted_msg)
print(f"SHA-1 hash of the recieved message: {hashed_message}")
print(decrypted_msg)
decrypted=decrypt2(public2,decrypted_hashed_msg)
are_equal = compare_two_hashes(hashed_message,decrypted)
print(f"Hashes are equal: {are_equal}")
encrypted_binary = read_encrypted_from_file()
write_decrypted_to_file(decrypted_msg)
'''
Euclid's algorithm for determining the greatest common divisor
'''

def gcd(a, b):


while b != 0:
a, b = b, a % b
return a

'''
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 exponent > 0:


if exponent % 2 == 1:
result = (result * base) % modulus
base = (base * base) % modulus
exponent = exponent // 2
return result

def multiplicative_inverse(e, phi):


"""Extended Euclidean Algorithm (Required)"""
old_r, r = e, phi
old_s, s = 1, 0

while r != 0:
quotient = old_r // r
old_r, r = r, old_r - quotient * r
old_s, s = s, old_s - quotient * s

return old_s % phi # This is d

def generate_key_pair(p, q):


n=p*q
phi = (p-1) * (q-1)
e = 65537 # Standard choice
d = multiplicative_inverse(e, phi)
return ((e, n), (d, n))

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

def encrypt(pk, plaintext):


"""Encrypt text using square-and-multiply"""
key, n = pk
cipher = [square_and_multiply(ord(char), key, n) for char in plaintext]
return ' '.join(map(str, cipher))

def encrypt2(pk, plaintext_hex):


# Unpack the public key into its components
key, n = pk
# Convert the hex string into bytes
try:
plaintext_bytes = bytes.fromhex(plaintext_hex)
except ValueError:
raise ValueError("Invalid hexadecimal string for plaintext")
# Encrypt each byte using the public key components
cipher = [pow(byte, key, n) for byte in plaintext_bytes]
return cipher

def decrypt2(prk, encrypted_message):


# Unpack the private key into its components
key, n = prk
# Convert the encrypted message string back to a list of integers
cipher = list(map(int, encrypted_message.split()))
# Decrypt each number using a^b mod m
plaintext = ''.join(chr(pow(char, key, n)) for char in cipher)
return plaintext

def decrypt(pk, ciphertext):


# Unpack the key into its components
key, n = pk

# Decrypt each element in the ciphertext using the RSA algorithm


decrypted_numbers = [pow(char, key, n) for char in ciphertext]

# Convert the list of decrypted numbers to a byte array


decrypted_bytes = bytearray()

for num in decrypted_numbers:


# Convert each number to a byte representation
byte_representation = num.to_bytes((num.bit_length() + 7) // 8, byteorder='big')
decrypted_bytes.extend(byte_representation)

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 :

def square_and_multiply(base: int, exponent: int, modulus: int) -> int:


"""Efficiently computes (base^exponent) mod modulus."""
result = 1
base = base % modulus # Ensure base is within modulus

while exponent > 0:


if exponent % 2 == 1:
result = (result * base) % modulus # Multiply step
base = (base * base) % modulus # Square step
exponent = exponent // 2 # Move to next bit

return result

My useage :

def encrypt(pk, plaintext):


"""Encrypts plaintext using RSA public key (e, n)."""
e, n = pk
cipher = [square_and_multiply(ord(char), e, n) for char in plaintext]
return ' '.join(map(str, cipher))

def decrypt(pk, ciphertext):


"""Decrypts ciphertext using RSA private key (d, n)."""
d, n = pk
decrypted_numbers = [square_and_multiply(int(char), d, n) for char in ciphertext.split()]

# Convert numbers back to bytes/string


decrypted_bytes = bytearray()
for num in decrypted_numbers:
byte_representation = num.to_bytes((num.bit_length() + 7) // 8, byteorder='big')
decrypted_bytes.extend(byte_representation)

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)

You might also like