Skip to content

Commit bd38a5d

Browse files
committed
Implemented CBC and ECB file encryption and corrected tests to work for file encryption as well.
Signed-off-by: Gabriel Lindeblad <Gabriel.lindeblad@icloud.com>
1 parent afe29fb commit bd38a5d

File tree

4 files changed

+175
-30
lines changed

4 files changed

+175
-30
lines changed

src/AES_Python/main.py

Lines changed: 159 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44
be used in any other use case than educational and no security is guaranteed for data
55
encrypted or decrypted using this tool.
66
"""
7+
import os
78

89
# Imported libraries
910
import numpy as np # Used for arrays and mathematical operations.
1011
import galois # Used for GF(2^8) multiplication in mix columns operation.
1112
from numpy.typing import NDArray # Used for type hinting numpy arrays.
1213
from typing import Any # Used for type hinting __getattr__ function.
1314
from secrets import token_bytes # Used for generating random key if needed.
15+
from os.path import getsize # Used to acquire size of files.
16+
from os import remove # Used to remove files.
1417

1518

1619
class AES:
@@ -138,7 +141,7 @@ def get(self, item: str) -> Any:
138141
:param item: Attribute to be retrieved. Valid attributes (running_mode, key, iv).
139142
:return: Attribute value.
140143
"""
141-
if item:
144+
if item in ["running_mode", "key", "iv"]:
142145
return self.__dict__[f"_{item}"]
143146
else:
144147
raise AttributeError(f"No attribute <{item}> exists!")
@@ -176,8 +179,17 @@ def enc(self, *, data_string: str = "", file_path: str = "",
176179
return self.__cbc_enc(data_string=data_string, keys=self.key_expand(key), iv=iv)
177180
else:
178181
raise NotImplementedError(f"{running_mode} is not supported!")
182+
elif file_path:
183+
if running_mode == "ECB":
184+
self.__ecb_enc(file_path=file_path, keys=self.key_expand(key))
185+
return ""
186+
elif running_mode == "CBC":
187+
self.__cbc_enc(file_path=file_path, keys=self.key_expand(key), iv=iv)
188+
return ""
189+
else:
190+
raise NotImplementedError(f"{running_mode} is not supported!")
179191
else:
180-
raise NotImplementedError("File encryption is not implemented yet...")
192+
raise RuntimeWarning("No file or string was give...")
181193

182194
def dec(self, *, data_string: str = "", file_path: str = "",
183195
running_mode: str = "", key: str = "", iv: str = "") -> str:
@@ -212,8 +224,17 @@ def dec(self, *, data_string: str = "", file_path: str = "",
212224
return self.__cbc_dec(data_string=data_string, keys=self.key_expand(key), iv=iv)
213225
else:
214226
raise NotImplementedError(f"{running_mode} is not supported!")
227+
elif file_path:
228+
if running_mode == "ECB":
229+
self.__ecb_dec(file_path=file_path, keys=self.key_expand(key))
230+
return ""
231+
elif running_mode == "CBC":
232+
self.__cbc_dec(file_path=file_path, keys=self.key_expand(key), iv=iv)
233+
return ""
234+
else:
235+
raise NotImplementedError(f"{running_mode} is not supported!")
215236
else:
216-
raise NotImplementedError("File encryption is not implemented yet...")
237+
raise RuntimeWarning("No file or string was give...")
217238

218239
@classmethod
219240
def __ecb_enc(cls, *, data_string: str = "", file_path: str = "", keys: NDArray[np.uint8]) -> str:
@@ -234,7 +255,7 @@ def __ecb_enc(cls, *, data_string: str = "", file_path: str = "", keys: NDArray[
234255
enc: str = "".join(cls.vec_chr(cls.__enc_schedule(raw, keys).flatten().astype(np.uint8)))
235256
output_string += enc
236257

237-
extra = len(data_string) % 16 # Calculates length of final data block
258+
extra = len(data_string) % 16 # Calculates length of final data block
238259
result: str = ""
239260

240261
if extra != 0: # If last data block not integer multiple of 16 adds extra padding
@@ -244,8 +265,33 @@ def __ecb_enc(cls, *, data_string: str = "", file_path: str = "", keys: NDArray[
244265
result = "".join(cls.vec_chr(cls.__enc_schedule(raw.reshape(4, 4), keys).flatten().astype(np.uint8)))
245266

246267
return output_string + result
268+
elif file_path:
269+
file_size = getsize(file_path)
270+
271+
with open(f"{file_path}.enc", 'wb') as output, open(file_path, 'rb') as data:
272+
for i in range(int(file_size / 16)):
273+
raw = np.array([i for i in data.read(16)], dtype=np.uint8).reshape(4, 4)
274+
out = bytes((cls.__enc_schedule(raw, keys).flatten()).tolist())
275+
output.write(out)
276+
277+
if file_size % 16 != 0:
278+
raw_l = [i for i in data.read()]
279+
raw_l, length = cls.__add_padding(raw_l)
280+
281+
out = bytes((cls.__enc_schedule(np.array(raw_l).reshape(4, 4), keys).flatten()).tolist())
282+
identifier = bytes((cls.__enc_schedule(np.array([0 for i in range(15)] + [length]).reshape(4, 4),
283+
keys).flatten()).tolist())
284+
285+
output.write(out + identifier)
286+
else:
287+
identifier = bytes((cls.__enc_schedule(np.array([0 for i in range(16)]).reshape(4, 4),
288+
keys).flatten()).tolist())
289+
output.write(identifier)
290+
291+
remove(file_path)
292+
return ""
247293
else:
248-
raise NotImplementedError
294+
raise RuntimeError("No string or file path received...?")
249295

250296
@classmethod
251297
def __ecb_dec(cls, *, data_string: str = "", file_path: str = "", keys: NDArray[np.uint8]) -> str:
@@ -268,8 +314,30 @@ def __ecb_dec(cls, *, data_string: str = "", file_path: str = "", keys: NDArray[
268314
output_string += dec
269315

270316
return output_string
317+
elif file_path:
318+
file_size = getsize(file_path)
319+
file_name = file_path[:-4]
320+
321+
with open(f"{file_name}", 'wb') as output, open(file_path, 'rb') as data:
322+
for i in range(int(file_size / 16) - 2):
323+
raw = np.array([i for i in data.read(16)]).reshape(4, 4)
324+
result = bytes((cls.__dec_schedule(raw, keys).flatten()).tolist())
325+
output.write(result)
326+
327+
data_final = np.array([i for i in data.read(16)]).reshape(4, 4)
328+
identifier = np.array([i for i in data.read()]).reshape(4, 4)
329+
330+
data_dec = (cls.__dec_schedule(data_final, keys).flatten()).tolist()
331+
id_l = (cls.__dec_schedule(identifier, keys).flatten()).tolist()
332+
333+
result = bytes(cls.__remove_padding(data_dec, id_l))
334+
335+
output.write(result)
336+
337+
remove(file_path)
338+
return ""
271339
else:
272-
raise NotImplementedError
340+
raise RuntimeError("No string or file path received...?")
273341

274342
@classmethod
275343
def __cbc_enc(cls, *, data_string: str = "", file_path: str = "", keys: NDArray[np.uint8], iv: str) -> str:
@@ -289,7 +357,7 @@ def __cbc_enc(cls, *, data_string: str = "", file_path: str = "", keys: NDArray[
289357
for i in range(len(data_string) // 16): # Encryption cycle, skips last if not integer multiple of 16 bytes
290358
raw: NDArray[np.uint8] = np.array([ord(i) for i in data_string[(i * 16): ((i + 1) * 16)]],
291359
dtype=np.uint8).reshape(4, 4)
292-
enc = np.bitwise_xor(raw, enc_array) # Xor operation with previous encrypted block or iv
360+
enc = np.bitwise_xor(raw, enc_array) # Xor operation with previous encrypted block or iv
293361
enc_array = cls.__enc_schedule(enc, keys)
294362
output_string += "".join(cls.vec_chr(enc_array.flatten().astype(np.uint8)))
295363

@@ -306,8 +374,35 @@ def __cbc_enc(cls, *, data_string: str = "", file_path: str = "", keys: NDArray[
306374
result = "".join(cls.vec_chr(cls.__enc_schedule(temp_array, keys).flatten().astype(np.uint8)))
307375

308376
return output_string + result
377+
elif file_path:
378+
file_size = getsize(file_path)
379+
380+
with open(f"{file_path}.enc", 'wb') as output, open(file_path, 'rb') as data:
381+
for i in range(int(file_size / 16)):
382+
raw = np.array([i for i in data.read(16)]).reshape(4, 4)
383+
raw = np.bitwise_xor(raw, enc_array)
384+
enc_array = cls.__enc_schedule(raw, keys)
385+
output.write(bytes((enc_array.flatten()).tolist()))
386+
387+
if file_size % 16 != 0:
388+
final = [i for i in data.read()]
389+
final, length = cls.__add_padding(final)
390+
391+
raw = np.bitwise_xor(np.array(final).reshape(4, 4), enc_array)
392+
enc_array = cls.__enc_schedule(raw, keys)
393+
394+
identifier = np.bitwise_xor(np.array([0 for i in range(15)] + [length]).reshape(4, 4), enc_array)
395+
identifier = cls.__enc_schedule(identifier, keys)
396+
397+
output.write(bytes((enc_array.flatten()).tolist() + (identifier.flatten()).tolist()))
398+
else:
399+
identifier = np.bitwise_xor(np.array([0 for i in range(16)]).reshape(4, 4), enc_array)
400+
id_bytes = bytes(((cls.__enc_schedule(identifier, keys)).flatten()).tolist())
401+
output.write(id_bytes)
402+
remove(file_path)
403+
return ""
309404
else:
310-
raise NotImplementedError
405+
raise RuntimeError("No string or file path received...?")
311406

312407
@classmethod
313408
def __cbc_dec(cls, *, data_string: str = "", file_path: str = "", keys: NDArray[np.uint8], iv: str) -> str:
@@ -325,7 +420,7 @@ def __cbc_dec(cls, *, data_string: str = "", file_path: str = "", keys: NDArray[
325420
output_string: str = ""
326421

327422
for i in range(len(data_string) // 16): # Decryption cycle
328-
raw: NDArray[np.uint8] = np.array( # Reads in input string 16 bytes at a time
423+
raw: NDArray[np.uint8] = np.array( # Reads in input string 16 bytes at a time
329424
[ord(i) for i in data_string[(i * 16): ((i + 1) * 16)]], dtype=np.uint8).reshape(4, 4)
330425

331426
temp_array = cls.__dec_schedule(raw, keys)
@@ -336,8 +431,44 @@ def __cbc_dec(cls, *, data_string: str = "", file_path: str = "", keys: NDArray[
336431
output_string += dec
337432

338433
return output_string
434+
elif file_path:
435+
file_size = getsize(file_path)
436+
file_name = file_path[:-4]
437+
438+
with open(f"{file_name}", 'wb') as output, open(file_path, 'rb') as data:
439+
if int(file_size / 16) - 3 >= 0:
440+
vector = np.array([i for i in data.read(16)]).reshape(4, 4)
441+
raw = cls.__dec_schedule(vector, keys)
442+
result = np.bitwise_xor(raw, dec_array)
443+
output.write(bytes((result.flatten()).tolist()))
444+
445+
for i in range(int(file_size / 16) - 3):
446+
raw = np.array([i for i in data.read(16)]).reshape(4, 4)
447+
result = cls.__dec_schedule(raw, keys)
448+
result = np.bitwise_xor(result, vector)
449+
vector = raw
450+
output.write(bytes((result.flatten()).tolist()))
451+
else:
452+
vector = dec_array
453+
454+
data_pice = np.array([i for i in data.read(16)]).reshape(4, 4)
455+
vector_1, identifier = data_pice, np.array([i for i in data.read()]).reshape(4, 4)
456+
457+
result = cls.__dec_schedule(data_pice, keys)
458+
identifier = cls.__dec_schedule(identifier, keys)
459+
460+
identifier = np.bitwise_xor(identifier, vector_1)
461+
data_pice = np.bitwise_xor(result, vector)
462+
463+
result_bytes = bytes(cls.__remove_padding((data_pice.flatten()).tolist(),
464+
(identifier.flatten()).tolist()))
465+
466+
output.write(result_bytes)
467+
468+
remove(file_path)
469+
return ""
339470
else:
340-
raise NotImplementedError
471+
raise RuntimeError("No string or file path received...?")
341472

342473
@staticmethod
343474
def key_gen(length: int = 16) -> str:
@@ -533,3 +664,21 @@ def __mix_columns(matrix: NDArray[np.uint8], shift: int) -> NDArray[np.uint8]:
533664
result[j, i] = temp
534665

535666
return result.transpose()
667+
668+
@staticmethod
669+
def __add_padding(data: list[int]) -> tuple[list[int], int]:
670+
"""Adds a padding to ensure a bloke size of 16 bytes."""
671+
length = 16 - len(data)
672+
for i in range(length):
673+
data.append(0)
674+
return data, length
675+
676+
@staticmethod
677+
def __remove_padding(data: list[int], identifier: list[int]) -> list[int]:
678+
"""Removes the padding from the data using information from identifier."""
679+
if identifier[-1] == 0:
680+
return data
681+
elif 0 < identifier[-1] < 16:
682+
return data[:-identifier[-1]]
683+
else:
684+
raise ValueError('Invalid padding')

temp/test.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
import numpy as np
2+
import os
23
from AES_Python import AES
34

45
aes_test = AES(running_mode="CBC",
56
key="2b7e151628aed2a6abf7158809cf4f3c",
6-
iv="000102030405060708090A0B0C0D0E0F"
7+
iv="000102030405060708090a0b0c0d0e0f"
78
)
89

9-
print(aes_test, "\n")
10+
data = b'\x1b\x16\x86:\xb9*w\xc5)"\xe4\xe9D\\\xf1\xee\x8b\x03\xcc\xe7\x0c~\xba7\xcf\x0f\x9c\x16dM$\xe9\x91\xef\xc3\xa6\xd2\xf0\xcd\xc2\xee\x86\xf0\x90\x8a]\x87\xf5R\xe2.c\xd4\xc6T\xdc\xe0#\xa7X\x8b_\x81\x04'
11+
file_name = "tmp.txt"
12+
expected = b'1234567890123456789012345678901234567890'
1013

11-
data_raw = '6bc1bee22e409f96e93d7e117393172a'
14+
with open(f"{file_name}.enc", "wb") as file:
15+
file.write(data)
1216

13-
data = "".join([chr(i) for i in bytes.fromhex(data_raw)])
17+
aes_test.dec(file_path=f"{file_name}.enc")
1418

15-
print("Original data:", data)
19+
with open(f"{file_name}", "rb") as file:
20+
result = file.read()
1621

17-
enc_data = aes_test.enc(data_string=data)
22+
os.remove(f"{file_name}")
1823

19-
print("Encrypted data:", bytearray([ord(i) for i in enc_data]).hex())
20-
21-
print("Expected:", "7649abac8119b246cee98e9b12e9197d")
22-
23-
dec_data = aes_test.dec(data_string=enc_data)
24-
25-
print("Decrypted data:", dec_data)
24+
print("Result:", result)
25+
print("Expected:", expected)

tests/test_file_dec.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from AES_Python import AES
44

55

6-
@pytest.mark.skip(reason="Not correctly implemented yet")
76
@pytest.mark.parametrize("data,key,file_name,expected", [
87
# 128 bit
98
(b'|\x94\x18\xcf\x1c\xf0\xef\xa0\xff\xa4\xbb\xe9\xd8\x8am\xa40f\xe4\x1eg\x9d\x88\xb8\xef\xeb{=J\xf3\xf6\xc1',
@@ -28,7 +27,7 @@ def test_file_dec_ecb(data, key, file_name, expected):
2827
file.write(data)
2928

3029
ecb = AES(key=key, running_mode="ECB")
31-
ecb.dec(file_path=file_name)
30+
ecb.dec(file_path=f"{file_name}.enc")
3231

3332
with open(file_name, "rb") as file:
3433
result = file.read()
@@ -38,7 +37,6 @@ def test_file_dec_ecb(data, key, file_name, expected):
3837
assert result == expected
3938

4039

41-
@pytest.mark.skip(reason="Not correctly implemented yet")
4240
@pytest.mark.parametrize("data,key,file_name,iv,expected", [
4341
# 128 bit
4442
(b'\xe4\xa7\x0e\xbd\x84\xfa\xf5\xd8`\xb8\xa1\x10\x0b~\xadh\x89Feso\xc5~_|\xe9\x1bG\xd9*\\\x81',
@@ -64,8 +62,8 @@ def test_file_dec_cbc(data, key, file_name, iv, expected):
6462
with open(f"{file_name}.enc", "wb") as file:
6563
file.write(data)
6664

67-
cbc = AES(key=key, running_mode="CBC")
68-
cbc.dec(file_path=file_name)
65+
cbc = AES(key=key, iv=iv, running_mode="CBC")
66+
cbc.dec(file_path=f"{file_name}.enc")
6967

7068
with open(file_name, "rb") as file:
7169
result = file.read()

tests/test_file_enc.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from AES_Python import AES
44

55

6-
@pytest.mark.skip(reason="Not correctly implemented yet")
76
@pytest.mark.parametrize("data,key,file_name,expected", [
87
# 128 bit
98
(b'1234567890', "2b7e151628aed2a6abf7158809cf4f3c", "tmp.txt",
@@ -36,7 +35,6 @@ def test_file_enc_ecb(data, key, file_name, expected):
3635
assert result == expected
3736

3837

39-
@pytest.mark.skip(reason="Not correctly implemented yet")
4038
@pytest.mark.parametrize("data,key,file_name,iv,expected", [
4139
# 128 bit
4240
(b'1234567890', "2b7e151628aed2a6abf7158809cf4f3c", "tmp.txt",

0 commit comments

Comments
 (0)