diff --git a/README.md b/README.md index 1affbf7..92e2b37 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ * [Enter the input](#enter-the-input) * [Run](#run) * [Configuration](#configuration) +* [Examples](#examples) + * [Encryption](#encryption-example) + * [Text Decryption](#text-decryption-example) + * [Image Decryption](#image-decryption-example) ## Description @@ -50,4 +54,97 @@ You can change the `POS_LEN` in the `config.py` file, which changes the input te Default is `POS_LEN = 4`, so the maximum length the text can have by default is **32,446** -The more the `POS_LEN` is, the longer the text can be, but the longer it will take to encrypt and decrypt it. \ No newline at end of file +The more the `POS_LEN` is, the longer the text can be, but the longer it will take to encrypt and decrypt it. + +| POS_LEN | Max text lenght | +|-|-| +| 1 | 2 | +| 2 | 107 | +| 3 | 2,026 | +| 4 | 32,446 | +| 5 | 524,169 | +| 6 | 8,386,749 | +| 7 | 134,188,028 | +| 8 | 2,147,329,544 | +| ... | ... | + + + + +## Examples + +### Encryption example + + + +##### `config.py` + +```python +POS_LEN = 2 +``` + +##### Input text (`input/test.txt`) +```txt +This is a test 123!!! + +It can have every UTF-8 character! ✔️ ❤️ ☆ +``` + +##### Password +```txt +test#@–{}password123¿? +``` + +##### Encrypted text (`output/encrypted_text.txt`) +```txt +3e8b00b4bcbb4ff246fdb3bc9afd63cf080e1cbefdc4b4bb2b5f1400f3fd4e6cb10d40825d0ab41e080e4e751a1ebbb8e7c4448fc14434d5c84d7fb3cc68e2c66033d5cfeece84bd256888b5e3dbb5bdc7fd47845be373e44bc8defbabb92e544f5eb0b4c43403084344d663 +``` + +##### Encrypted image (`output/encrypted_image.png`) + + + + +### Text Decryption example + + + +##### Input text (`input/encrypted_text.txt`) +```txt +This is a test 123!!! + +It can have every UTF-8 character! ✔️ ❤️ ☆ +``` + +##### Password +```txt +test#@–{}password123¿? +``` + +##### Decrypted text (`output/decrypted_text.txt`) +```txt +This is a test 123!!! + +The text can have every UTF-8 character! ✔️ ❤️ ☆ +``` + + + +### Image Decryption example + + + +##### Input image (`input/encrypted_image.png`) + + +##### Password +```txt +test#@–{}password123¿? +``` + +##### Decrypted image (`output/decrypted_image.txt`) +```txt +This is a test 123!!! + +The text can have every UTF-8 character! ✔️ ❤️ ☆ +``` \ No newline at end of file diff --git a/classes/Decryptor.py b/classes/Decryptor.py new file mode 100644 index 0000000..8e7e075 --- /dev/null +++ b/classes/Decryptor.py @@ -0,0 +1,38 @@ +import random + +import sys; sys.path.append("..") +from constants.constants import * +from utils.cryptography import * + +class Decryptor: + def __init__(self, ciphertext:str, pwd:str): + self.ciphertext = ciphertext + self.pwd = pwd + self.hash = get_pwd_hash(pwd) + + + def decrypt(self) -> str: + cipher_len = len(self.ciphertext) # Same as CIPHER_LEN + + if not all(c in HEX_SYMB for c in self.ciphertext) or len(self.ciphertext) != cipher_len: + raise Exception("Encrypted text is invalid") + + # Seed the random generator with the hash + seed = int(self.hash,16) + random.seed(seed) + + # Get the encryption code + encryption_code = get_encryption_code(self.hash) + + # Decrypt the text length + textE_len_digits = len(format(cipher_len,"x")) # If the position length is 5, the length of the encrypted text will fit in 5 digits + textE_len_idxs = get_textE_len_idxs(seed, cipher_len, textE_len_digits) + textE_lenE = "".join([self.ciphertext[idx] for idx in textE_len_idxs]) + textE_len = int(dec(textE_lenE, encryption_code), 16) + + # Decrypt the text + textE_idxs = get_textE_idxs(seed, cipher_len, textE_len, textE_len_idxs) + textE = "".join([self.ciphertext[idx] for idx in textE_idxs]) + text = h2t(dec(textE, encryption_code)) + + return text \ No newline at end of file diff --git a/classes/Encryptor.py b/classes/Encryptor.py new file mode 100644 index 0000000..ff39d38 --- /dev/null +++ b/classes/Encryptor.py @@ -0,0 +1,47 @@ +import random + +import sys; sys.path.append("..") +from constants.constants import * +from utils.cryptography import * + +class Encryptor: + def __init__(self, text:str, pwd:str): + self.text = text + self.pwd = pwd + self.hash = get_pwd_hash(self.pwd) + + + def encrypt(self) -> str: # Returns a ciphertext + if len(self.text)>TXT_MAX_LEN or len(self.text)==0: + raise Exception("Text length is invalid") + + # Seed the random generator with the hash + seed = int(self.hash,16) + random.seed(seed) + + # Get the encryption code + encryption_code = get_encryption_code(self.hash) + + # Fill the ciphertext with random characters + ciphertext = [random.choice(HEX_SYMB) for _ in range(CIPHER_LEN)] + + # INFO: Encrypt the text + textE = enc(t2h(self.text), encryption_code) + + # INFO: Encrypt the text length + textE_len = len(textE) + textE_len_max_digits = len(format(CIPHER_LEN,"x")) # If the position length is 5, the length of the encrypted text will fit in 5 digits + textE_len_fixed_len = format(textE_len,"x").rjust(textE_len_max_digits,"0") + textE_lenE = enc(textE_len_fixed_len, encryption_code) # Encrypted text length (fixed length) + + # Get the indexes where the INFO will be stored + textE_len_idxs = get_textE_len_idxs(seed, CIPHER_LEN, textE_len_max_digits) + textE_idxs = get_textE_idxs(seed, CIPHER_LEN, len(textE), textE_len_idxs) + + # Save the text and text length in toret + for i,idx in enumerate(textE_len_idxs): + ciphertext[idx] = textE_lenE[i] + for i,idx in enumerate(textE_idxs): + ciphertext[idx] = textE[i] + + return "".join(ciphertext) \ No newline at end of file diff --git a/classes/ImageCreator.py b/classes/ImageCreator.py new file mode 100644 index 0000000..f780f04 --- /dev/null +++ b/classes/ImageCreator.py @@ -0,0 +1,18 @@ +import numpy as np + +import sys; sys.path.append("..") +from constants.constants import * + +class ImageCreator: + def __init__(self, text:str): + self.text = text + + + def get_img_arr(self) -> np.ndarray: + img_arr = np.array( + [[int(self.text[i:i+2], 16), + int(self.text[i+2:i+4], 16), + int(self.text[i+4:i+6], 16)] + for i in range(0, len(self.text), 6)], + dtype=np.uint8) + return img_arr.reshape(IMG_SIZE, IMG_SIZE, 3) \ No newline at end of file diff --git a/classes/Menu.py b/classes/Menu.py new file mode 100644 index 0000000..1b44005 --- /dev/null +++ b/classes/Menu.py @@ -0,0 +1,117 @@ +import os +from colorama import Fore, init; init() +from PIL import Image + +from .Text import Text +from .Options import Options +from .Encryptor import Encryptor +from .Decryptor import Decryptor +from .ImageCreator import ImageCreator + +import sys; sys.path.append("..") +from constants.constants import * + +class Menu(): + def __init__(self): + self.create_folders() + print(f'\n{Text("Max length of the text",Fore.CYAN)}: {Text(f"{TXT_MAX_LEN:,}",Fore.GREEN)} (you can change it in {Text("config.py",Fore.LIGHTYELLOW_EX)}') + option = Options(["EXIT", "Encrypt", "Decrypt text", "Decrypt image"]).get_choice() + + if option == 0: + exit() + + elif option == 1: + self.encrypt() + + elif option == 2: + self.decrypt_text() + + elif option == 3: + self.decrypt_image() + + + def create_folders(self): + if not os.path.exists(INPUT_DIR): os.mkdir(INPUT_DIR) + if not os.path.exists(OUTPUT_DIR): os.mkdir(OUTPUT_DIR) + + + def encrypt(self): + text = open(self.get_input_file("Text filename: ", "txt")).read() + pwd = input("Password: ") + enc_text_file = self.get_file("New text filename: ", "txt") + enc_img_file = self.get_file("New image filename: ", "png") if self.yes_no("Save image? [y/n]: ") else None + + try: + encrypted_text = Encryptor(text, pwd).encrypt() + self.save_text(encrypted_text, enc_text_file) + print(Text("\nText encrypted succesfully\n", Fore.GREEN)) + if enc_img_file: + self.save_img(encrypted_text, enc_img_file) + print(Text("Image saved", Fore.MAGENTA)) + except Exception as e: + print(Text(f"Error: {e}", Fore.RED)) + return + + + def decrypt(self, ciphertext:str) -> str: + pwd = input("Password: ") + dec_text_file = self.get_file("New filename: ", "txt") + + try: + decypted_text = Decryptor(ciphertext, pwd).decrypt() + self.save_text(decypted_text, dec_text_file) + print(Text("\nText decrypted succesfully\n", Fore.GREEN)) + except Exception as e: + print(Text(f"Error: {e}", Fore.RED)) + return + + + def decrypt_text(self): + ciphertext = open(self.get_input_file("Ciphertext filename: ", "txt")).read() + self.decrypt(ciphertext) + + + def decrypt_image(self): + ciphertext = self.img_to_text(self.get_input_file("Cipher image filename: ", "png")) + self.decrypt(ciphertext) + + + def img_to_text(self, img_file:str) -> str: + img_arr = np.array(Image.open(img_file)).flatten() + img_str = "".join([f'{n:02x}' for n in img_arr]) + return img_str + + + def yes_no(self, msg:str="Save? [y/n]: ") -> bool: + inp = input(msg) + if inp.lower() == "y": return True + elif inp.lower() == "n": return False + else: return self.yes_no(msg) + + + def save_text(self, text:str, file:str): + with open(f"{OUTPUT_DIR}/{file}", "w") as f: + f.write(text) + + + def save_img(self, text:str, file:str): + img_arr = ImageCreator(text).get_img_arr() + Image.fromarray(img_arr).save(f"{OUTPUT_DIR}/{file}") + + + def get_file(self, msg:str, ext:str) -> str: + while True: + filename = input(msg) + if "." in filename: + if filename.endswith(f".{ext}"): return filename + print(Text("Invalid extension", Fore.RED)) + continue + return f"{filename}.{ext}" + + + def get_input_file(self, msg:str, ext:str) -> str: + while True: + file = self.get_file(msg, ext) + path = f"{INPUT_DIR}/{file}" + if os.path.exists(path): return path + print(Text("File not found",Fore.RED)) \ No newline at end of file diff --git a/classes/Options.py b/classes/Options.py new file mode 100644 index 0000000..eaaccdb --- /dev/null +++ b/classes/Options.py @@ -0,0 +1,28 @@ +from colorama import Fore, init; init() +from .Text import Text + +class Options: + def __init__(self, options:list, first_idx=0): + self.options = options + self.first_idx = first_idx + + + def get_choice(self) -> int: + self.__str__() + choice = input("Option: ") + + if self.check_input(choice): return int(choice) + + print(Text("\nInvalid choice.\n", Fore.RED)) + return self.get_choice() + + + def check_input(self, choice:str) -> bool: + return choice.strip().isdigit() and \ + int(choice) in range(self.first_idx, self.first_idx + len(self.options)) + + + def __str__(self) -> str: + print("".join( + [f"[{self.first_idx+i}] {opt}\n" + for i,opt in enumerate(self.options)])) \ No newline at end of file diff --git a/classes/Text.py b/classes/Text.py new file mode 100644 index 0000000..97868a7 --- /dev/null +++ b/classes/Text.py @@ -0,0 +1,9 @@ +from colorama import Fore, init; init() + +class Text: + def __init__(self, text:str, color:str): + self.text = text + self.color = color + + def __str__(self) -> str: + return f"{self.color}{self.text}{Fore.RESET}" \ No newline at end of file diff --git a/config.py b/config.py index 698331d..40388b9 100644 --- a/config.py +++ b/config.py @@ -1,4 +1,4 @@ -POS_LEN = 4 +POS_LEN = 2 """ The length that the positions in the encryption will have (in hex) diff --git a/constants/constants.py b/constants/constants.py new file mode 100644 index 0000000..a83acf8 --- /dev/null +++ b/constants/constants.py @@ -0,0 +1,14 @@ +import numpy as np + +import sys; sys.path.append("..") +from config import * + +INPUT_DIR = "input" +OUTPUT_DIR = "output" + +HEX_SYMB = "0123456789abcdef" + +IMG_SIZE = int(np.sqrt((16**POS_LEN)//6)) # Size that the image will have (the image is a square) +CIPHER_LEN = (IMG_SIZE**2)*6 # Length that the ciphertext will have +TXT_E_MAX_LEN = CIPHER_LEN - len(format(CIPHER_LEN,"x")) # Max length text (encrypted) can have +TXT_MAX_LEN = TXT_E_MAX_LEN//2 # Max length the input text can have (Because the encrypted text is twice the length of the input text) \ No newline at end of file diff --git a/input/test.txt b/input/test.txt new file mode 100644 index 0000000..d7ffcf1 --- /dev/null +++ b/input/test.txt @@ -0,0 +1,3 @@ +This is a test 123!!! + +The text can have every UTF-8 character! ✔️ ❤️ ☆ \ No newline at end of file diff --git a/readme-assets/encrypted_image.png b/readme-assets/encrypted_image.png new file mode 100644 index 0000000..a85cfb3 Binary files /dev/null and b/readme-assets/encrypted_image.png differ diff --git a/readme-assets/example_encryption.png b/readme-assets/example_encryption.png new file mode 100644 index 0000000..66929a6 Binary files /dev/null and b/readme-assets/example_encryption.png differ diff --git a/readme-assets/example_image_decryption.png b/readme-assets/example_image_decryption.png new file mode 100644 index 0000000..7c80632 Binary files /dev/null and b/readme-assets/example_image_decryption.png differ diff --git a/readme-assets/example_text_decryption.png b/readme-assets/example_text_decryption.png new file mode 100644 index 0000000..9930147 Binary files /dev/null and b/readme-assets/example_text_decryption.png differ diff --git a/utils/cryptography.py b/utils/cryptography.py new file mode 100644 index 0000000..cdf09fc --- /dev/null +++ b/utils/cryptography.py @@ -0,0 +1,63 @@ +import hashlib +import sys; sys.path.append("..") +from constants.constants import * +import random + +def enc(txt, encryption_code:str) -> str: + encTxt = "" + for x in txt: + encTxt += encryption_code[int(x,16)] # The symbol in the equivalent position + return encTxt + + +# Decrypt an encrypted text with an encryption code +def dec(txt, encryption:str) -> str: + decTxt = "" + for x in txt: + decTxt += HEX_SYMB[encryption.index(x)] # The symbol in the equivalent position + return decTxt + + +# Encode the password to sha512 and get a hash (length = 128) +def get_pwd_hash(pwd:str) -> str: + return hashlib.sha512(pwd.encode()).hexdigest() + + +def get_encryption_code(hash:str) -> str: + encryption_code = "" + while len(encryption_code) < 16: # While the encryption string doesn't have 16 symbols + if (hash[0] not in encryption_code): + encryption_code += hash[0] + hash = h2h(hash) + return encryption_code + + +def get_textE_len_idxs(seed:int, encryption_len:int, textE_len_digits:int) -> tuple: # Save txt_len_enc in the first POS_LEN indexes + random.seed(seed) + return random.sample(range(encryption_len), textE_len_digits) + +def get_textE_idxs(seed:int, encryption_len:int, textE_len:int, textE_len_idxs:tuple) -> tuple: + random.seed(seed) + textE_idxs = [] + for _ in range(textE_len): + while True: + idx = random.randint(0, encryption_len-1) + if idx not in textE_idxs and idx not in textE_len_idxs: + textE_idxs.append(idx) + break + return textE_idxs + + +# Create a new hash from a hash +def h2h(hash:str, iters:int=1) -> str: + for _ in range(iters): + hash = hashlib.sha512(hash.encode()).hexdigest() + return hash + +# Convert from text to hexadecimal +def t2h(txt:str) -> str: + return txt.encode('utf-8').hex() + +# Convert hexadecimal to utf-8 +def h2t(hex:str)-> str: + return bytes.fromhex(hex).decode('utf-8') \ No newline at end of file