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