diff --git a/README.md b/README.md index d89cd34..b44781e 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,213 @@ -# Python_MessageEncoder -This simple script creates an encoded message that can be decoded with a code sample the script provides. -This uses the seed functionality in python random library. +# Pseudo-Random Encryption +This script provides Python functions for symmetric encryption and decryption using pseudo-random number generation. -## Usage +By default, the script takes in a message to encrypt and prints 2 lines of python code to `stdout`, +which when executed will give back the original message. e.g. `python main.py ishaan` outputs the following +```python +from random import randrange, seed; seed('5822100138856949'); m = 'b\x04v\x1b\x16\x05\x03\x00a\x00\x0e@4P' +print(''.join((chr(randrange(128)^ord(c))for c in m if not randrange(2))),end='') ``` -usage: encode.py [-h] [--file FILE] [--msg MSG] [--seed SEED] [--size SIZE] [--char_range CHAR_RANGE] [--output OUTPUT] [--garbage GARBAGE] +The script also offers modularity, allowing each function to be used independently as needed. + +The key in this encryption system is a tuple of 3 values: `seed`, `size_multiplier` and `char_range`. +They are interpreted as: +- `seed`: main `seed` value for encryption, passed to python's random library as is. +- `size_multiplier`: size multiplier for encrypted message. i.e. $\frac{\text{length of encrypted message}}{\text{length of original message}} \approx \text{size multiplier}$ +- `char_range`: unicode character range for each `char` of encrypted message. Does not impose any limitation on character range of original message. + +## Usage +
+Try: python main.py -h for help message and usage instructions. +
+usage: Pseudo-Random Encryptor [-h] [--hex] [--file] [--decrypt] [--values-only] [--seed SEED] [--size SIZE] [--char-range CHAR_RANGE]
+                               [--output OUTPUT] [--garbage GARBAGE]
+                               arg
+
+positional arguments:
+  arg                   The text to be encrypted, or the path of the file if -f flag is set.
 
 options:
-  -h, --help        show this help message and exit
-  -f, --file        Specify a text file whose contents are to be encoded
-  -m, --msg         The text to be encoded
-  -s, --seed        The seed for encoding [default:random]
-  -z, --size        The size_seed for encoding [default:2]
-                        It determines how large to make the encoded string.
-                        The encoded message is "roughly" 'size_seed' times the length of actual message
-  -c, --char_range  The range of unicode characters to be used for encoded the message [default:256]
-  -o, --output      Specify a text file where the output code will be saved
-  -g, --garbage     Seed for garbage values
-```
-## Example
-```
-python encode.py -m message -c 256 -g garbage_seed
-python encode.py -f file -s seed -c 256 -z 3 -o out.py
+  -h, --help            show this help message and exit
+  --hex, -x             [default:False] Indicates that the encrypted message is to be hex encoded. (or the message is to be decoded from
+                        hex, if -d is set)
+  --file, -f            [default:False] Indicate that the given argument is a file path insead of string.
+  --decrypt, -d         [default:False] Indicate that the message is to be decrypted, not encrypted.
+  --values-only, -vo    [default:False] Give key values and encrypted message only, not the entire code (Ignored if -d flag is set).
+  --seed SEED, -s SEED  [default:random] The seed for encrypting/decrypting. Passed to random library as seed.
+  --size SIZE, -z SIZE  [default:2] Approximately equal to len(encrypted) / len(original).
+  --char-range CHAR_RANGE, -r CHAR_RANGE
+                        [default:128] Unicode character range of the encrypted message (Does not limit the original message).
+  --output OUTPUT, -o OUTPUT
+                        [default:stdout] Specify a text file where the output code/values will be saved.
+  --garbage GARBAGE, -g GARBAGE
+                        [default:random] Seed for garbage values which randomize encrypted text.
+
+ +
+ +## Functionality +### Encrypting a Message +The `encrypt` function takes a message and encrypts it based on specified parameters, including: +- Seed: Main key for encryption, passed to the random library as a seed. +- Size Multiplier: Factor determining the size of the encrypted message. +- Character Range: Unicode character range of the encrypted message. +- Garbage Seed: Seed for randomizing the output. +- Hex Encoding: Indicates whether the output should be hex encoded. + +### Decrypting an Encrypted Message +The `decrypt` function decrypts an encrypted message based on given parameters, including: +- Seed: Main key for decryption, passed to the random library as a seed. +- Size Multiplier: Factor determining the size of the original message. +- Character Range: Unicode character range of the encrypted message. + +### Generating Python Code for Decryption +The `give_code` function generates Python code to decrypt a message based on specified parameters, including: +- Seed: Main key for decryption, passed to the random library as a seed. +- Size Multiplier: Factor determining the size of the original message. +- Character Range: Unicode character range of the encrypted message. +- Hex Encoding: Indicates whether the encrypted message is hex encoded. + +### Generating Key Values for Decryption +The `give_values` function generates a formatted string containing encrypted message and key values for decryption, including: + +- Seed: Main key for decryption, passed to the random library as a seed. +- Size Multiplier: Factor determining the size of the original message. +- Character Range: Unicode character range of the encrypted message. +- Hex Encoding: Indicates whether the output message is hex encoded. + +### Input Handling +The `get_input` function sets up an argument parser and returns parsed arguments in dictionary form. + +### Main Functionality +The `run` function serves as the main entry point, handling the encryption, decryption, and output functionalities based on user inputs and parameters. + +## Examples + +1. With no flags +```sh +$ python main.py message +``` +```python +from random import randrange, seed; seed('13234983975254888'); m = '2LA2\x01D\x1e\x0eQ8y\x1eB\x1d+r>\x16' +print(''.join((chr(randrange(128)^ord(c))for c in m if not randrange(2))),end='') +``` +2. Get only the encoded message and key values +```sh +$ python main.py message --values-only +``` +```text +message: "\x17\rk#\\/\r\x1b\x01$)\x1fC='(" +seed: '5008982697256702' +size seed: 2 +char range: 128 +hex encoded: False +``` +3. Write to an output file +```sh +$ python main.py "secret message" -o output.py +$ python output.py +``` +```text +secret message +``` +4. Read from file +```sh +$ python main.py -f input.txt -o output.py +$ python output.py > decoded.txt +$ diff decoded.txt input.txt +``` +```sh +# no diff +``` +5. Specify a size multiplier +```sh +$ python main.py "secret" --seed 0 --size 5 -vo -g 0 +``` +```text +message: 'U2N5|^0\x0e>\x1cO\x03\x1cjM@\x1d|ddf+\\' +seed: '0' +size seed: 5 +char range: 128 +hex encoded: False +``` +```sh +$ python main.py "secret" --seed 0 --size 1 -vo -g 0 +``` +```text +message: 'g9su\x057' +seed: '0' +size seed: 1 +char range: 128 +hex encoded: False +``` +6. Specifying garbage seed +```sh +$ python main.py "secret" --seed 0 -vo -g 0 +``` +```text +message: '-2$ud\x12&\x1a' +seed: '0' +size seed: 2 +char range: 128 +hex encoded: False +``` +```sh +$ python main.py "secret" --seed 0 -vo -g 1 +``` +```text +message: '\x1e2Bud\x12&\x1a' +seed: '0' +size seed: 2 +char range: 128 +hex encoded: False +``` +7. Decrypting an Encrypted Message +```text +message: '}/xf\x01-' +seed: '0' +size seed: 1 +char range: 128 +hex encoded: False +``` +```sh +$ python main.py -d '}/xf\x01-' -s 0 -z 1 +``` +```text +ishaan +``` +8. Using hex values +```sh +$ python main.py 'ishaan' -vo -s 0 -z 1 -x +``` +```text +message: '7D2F7866012D' +seed: '0' +size seed: 1 +char range: 128 +hex encoded: True +``` +```sh +$ python main.py -d '7D2F7866012D' -vo -s 0 -z 1 -x +``` +```text +ishaan +``` +9. Customizing character range +```sh +$ python main.py "secret" --seed 0 --char-range 1024 -vo +``` +```text +message: 'ͱɾϧâ]ͱɿ̃' +seed: '0' +size seed: 2 +char range: 1024 +hex encoded: False +``` +10. Decrypting a file +```sh +$ python main.py -d /path/to/file.enc -vo -s 0 -z 1 -o /path/to/file.dec +$ cat /path/to/file.dec +``` +```text +super secret text in file ``` diff --git a/encode.py b/encode.py deleted file mode 100644 index 3d25d76..0000000 --- a/encode.py +++ /dev/null @@ -1,195 +0,0 @@ -""" -`python encode.py -h` for help message -It encodes a given message (or contents of a given file) and provides a python code to extract the original message -It uses python random module and XOR (^) operator to encode the message -""" - -from random import Random -from typing import Any - - -def encode( - message: str, - seed: Any = 0, - size_seed: int = 2, - char_range: int = 256, - garbage_seed: Any = None, -) -> str: - """\ - Encodes the given message - The feild 'seed' is the seed for randomisation. - The feild 'size_seed' determines how large to make the encoded string - The encoded message is "roughly" 'size_seed' times the length of actual message - The feild 'char_range' determines the range of unicode characters to be used for encoded the message - The feild 'garbage_seed' is the seed for garbage values. - If you intend to get the same encoded message again pass same value to this feild. - NOTE: The same 'seed', 'size_seed' and 'char_range' values are required for decoding - """ - temp = Random(garbage_seed) - determined = Random(seed) - result = "" - i = 0 - n = len(message) - while i < n: - char = message[i] - if not determined.randrange(size_seed): - i += 1 - result += chr(determined.randrange(char_range) ^ ord(char)) - else: - result += chr(temp.randrange(char_range)) - return result.encode().hex().upper() - - -def validate( - seed: Any = 0, - size_seed: int = 2, - char_range: int = 256, -) -> tuple[Any, int, int]: - """ - Validate the values passed by the user - If seed is None, then the encryption will not be consistent and the algorithm will fail - """ - if seed is None: seed = 0 - size_seed = max(size_seed, 1) - if char_range < 1: char_range = 256 - return seed, size_seed, char_range - - -def decode( - encoded: str, - seed: Any = 0, - size_seed: int = 2, - char_range: int = 256, -) -> str: - """\ - Decodes the encoded message based on the given parameters - Please use the same values of 'seed', 'size_seed' and 'char_range' as used while encoding - """ - determined = Random(seed) - # return "".join((chr(determined.randrange(char_range) ^ ord(char)) for char in bytes.fromhex(encoded).decode() if not determined.randrange(size_seed))) - message = "" - for char in bytes.fromhex(encoded).decode(): - if not determined.randrange(size_seed): - message += chr(determined.randrange(char_range) ^ ord(char)) - return message - - -def give_code(encoded_message: str, seed: Any, size_seed: int, char_range: int) -> str: - """\ - This function gives the python code that can be used to decode the message based on given parameters - """ - return f"""import random; random.seed({seed!r}); m = {encoded_message!r} -print(''.join((chr(random.randrange({char_range})^ord(c))for c in bytes.fromhex(m).decode()if not random.randrange({size_seed})))) -""" - - -def give_values(encoded_message: str, seed: Any, size_seed: int, char_range: int) -> str: - return f"message:\t{encoded_message}\nseed:\t\t{seed}\nsize seed:\t{size_seed}\nchar range:\t{char_range}" - - -def get_input(): - import argparse - - parser = argparse.ArgumentParser() - parser.add_argument( - "arg", - type=str, - default="", - help="The text to be encoded, or the path of the file if -f flag is set", - ) - parser.add_argument( - "--file", - "-f", - action="store_true", - default=False, - help="If the argument is a file", - ) - parser.add_argument( - "--seed", - "-s", - default=f"{Random().random()}"[3:], - help="The seed for encoding [default:random]", - ) - parser.add_argument( - "--size", - "-z", - default=2, - type=int, - help="The size_seed for encoding [default:2]\nIt determines how large to make the encoded string.\n\tThe encoded message is \"roughly\" 'size_seed' times the length of actual message", - ) - parser.add_argument( - "--char_range", - "-r", - type=int, - default="256", - help="The range of unicode characters to be used for encoded the message [default:256]", - ) - parser.add_argument( - "--output", - "-o", - type=str, - default="", - help="Specify a text file where the output code will be saved", - ) - parser.add_argument( - "--garbage", - "-g", - default=None, - help="Seed for garbage values", - ) - parser.add_argument( - "--values-only", - "-vo", - default=False, - action="store_true", - help="Give values only, not the entire code", - ) - return parser.parse_args() - - -def _extract_message(args) -> str | int: - if args.file: - try: - with open(args.arg) as f: - message = f.read() - except FileNotFoundError: - print("File does not exist") - return -1 - else: - message = args.arg - return message if message else -1 - - -def _extract_keys(args): - seed, size_seed, char_range = validate(args.seed, args.size, args.char_range) - try: garbage_seed = args.garbage - except: garbage_seed = None - return seed, size_seed, char_range, garbage_seed - - -def run(args): - message = _extract_message(args) - if message == -1: # file does not exist or empty file - print("No message to encode") - return - - # keys = (seed, size_seed, char_range) - *keys, garbage_seed = _extract_keys(args) - encoded_message = encode(message, *keys, garbage_seed) - - output_tuple = (encoded_message, *keys) - output = ( - give_values(*output_tuple) if args.values_only else give_code(*output_tuple) - ) - if args.output: - try: - with open(args.output, "w") as f: - f.write(output) - except Exception as err: - print(f"Error occured while writing to file: {err}") - else: - print(f"\n{output}") - - -if __name__ == "__main__": - run(get_input()) diff --git a/main.py b/main.py new file mode 100644 index 0000000..4fa0137 --- /dev/null +++ b/main.py @@ -0,0 +1,272 @@ +""" +`python main.py -h` for help message +Uses python random module and XOR operation to encrypt/decrypt +The script is modular and each function can be used independently +""" + +from argparse import ArgumentParser +from random import Random + +defaults = { + "seed": 0, + "size_multiplier": 2, + "char_range": 128, + "garbage_seed": None, + "hex": False, # When hex is True, encrypted message is outputted longer than multiplier because multiple hex characters correspond to a single character +} + + +help_messages = { + "arg": "The text to be encrypted, or the path of the file if -f flag is set.", + "file": "[default:False] Indicate that the given argument is a file path insead of string.", + "decrypt": "[default:False] Indicate that the message is to be decrypted, not encrypted.", + "output": "[default:stdout] Specify a text file where the output code/values will be saved.", + "hex": "[default:False] Indicates that the encrypted message is to be hex encoded. (or the message is to be decoded from hex, if -d is set)", + "seed": "[default:random] The seed for encrypting/decrypting. Passed to random library as seed.", + "size": "[default:2] Approximately equal to len(encrypted) / len(original).", + "char range": "[default:128] Unicode character range of the encrypted message (Does not limit the original message).", + "garbage seed": "[default:random] Seed for garbage values which randomize encrypted text.", + "values only": "[default:False] Give key values and encrypted message only, not the entire code (Ignored if -d flag is set).", +} + + +def _load_defaults(params: dict, *args: str) -> None: + """\ + Load default values of given args into params + + Args: + params (dict): The dictionary to load defaults into + args (str): The arguments to load defaults of + + Returns: + None: makes changes in params itself + """ + for arg in args: + params[arg] = params.get(arg, defaults[arg]) + + +def _validate(params: dict) -> None: + """ + Validate the values passed by the user (as params) + + Args: + seed (Any): If value is None, the system fails + size_multiplier (int): Min value is 1 + char_range (int): Min value is 1 + + Returns: + None: makes changes in params itself + """ + args = ("seed", "size_multiplier", "char_range") + _load_defaults(params, *args) + + if params["seed"] is None: + params["seed"] = defaults["seed"] + if params["size_multiplier"] < 1: + params["size_multiplier"] = defaults["size_multiplier"] + if params["char_range"] < 1: + params["char_range"] = defaults["char_range"] + + +def encrypt(message: str, **params) -> str: + """\ + Encrypts the given message based on given parameters + + Args: + message (str): The string to encrypt + seed (Any): Main key for encryption, passed to random library as seed + size_multiplier (int): Factor of how large the encrypted message should be + char_range (int): Unicode character range of the encrypted message, does not limit the input message + garbage_seed (Any): Seed for randomizing the output, to replicate an output, use the same value + hex (bool): If the output should be hex encoded + + Returns: + str: The encrypted message + + NOTE: Ensure to use the same 'seed', 'size_multiplier', and 'char_range' values as used for decoding + """ + + args = ("seed", "size_multiplier", "char_range", "garbage_seed", "hex") + _load_defaults(params, *args) + + temp = Random(params["garbage_seed"]) + determined = Random(params["seed"]) + result = "" + size_multiplier = params["size_multiplier"] + char_range = params["char_range"] + i = 0 + n = len(message) + while i < n: + char = message[i] + if not determined.randrange(size_multiplier): + i += 1 + result += chr(determined.randrange(char_range) ^ ord(char)) + else: + result += chr(temp.randrange(char_range)) + if params.get("hex"): + result = result.encode().hex().upper() + return result + + +def decrypt(encrypted_message: str, **params) -> str: + """\ + Decrypts the encrypted message based on the given parameters + + Args: + encrypted_message (str): The encrypted message to be decrypted + seed (Any): Main key for decryption, passed to random library as seed + size_multiplier (int): Factor of how small the original message is + char_range (int): Unicode character range of the encrypted message + + Returns: + str: The decrypted message + + NOTE: Ensure to use the same 'seed', 'size_multiplier', and 'char_range' values as used while encoding. + """ + + args = ("seed", "size_multiplier", "char_range") + _load_defaults(params, *args) + + determined = Random(params["seed"]) + + size_multiplier = params["size_multiplier"] + char_range = params["char_range"] + message = "" + for char in encrypted_message: + if not determined.randrange(size_multiplier): + message += chr(determined.randrange(char_range) ^ char) + + return message + + +def give_code(encrypted_message: str, **params) -> str: + """\ + Generates Python code to decrypt the message based on given parameters + + Args: + encrypted_message (str): The encrypted message + seed (Any): Main key for decryption, passed to random library as seed + size_multiplier (int): Factor of how small the original message is + char_range (int): Unicode character range of the encrypted message + hex (bool): If the encrypted message is hex encoded + + Returns: + str: 2 lines of Python code for decoding the message + + NOTE: Ensure to use the same 'seed', 'size_multiplier', and 'char_range' values as used while encoding. + """ + args = ("seed", "size_multiplier", "char_range", "hex") + _load_defaults(params, *args) + + m = "bytes.fromhex(m).decode()" if params["hex"] else "m" + return f"""from random import randrange, seed; seed({params["seed"]!r}); m = {encrypted_message!r} +print(''.join((chr(randrange({params.get("char_range")})^ord(c))for c in {m} if not randrange({params.get("size_multiplier")}))),end='')""" + + +def give_values(encrypted_message: str, **params) -> str: + """\ + Generates a formatted string containing encrypted message and key values for decryption + + Args: + encrypted_message (str): The encrypted message + seed (Any): Main key for decryption, passed to random library as seed + size_multiplier (int): Factor of how small the original message is + char_range (int): Unicode character range of the encrypted message + hex (bool): Indicates weather the output message is hex encoded + + Returns: + str: The formatted string containing key values + + NOTE: Ensure to use the same 'seed', 'size_multiplier', and 'char_range' values as used while encoding. + """ + args = ("seed", "size_multiplier", "char_range", "hex") + _load_defaults(params, *args) + + return f"message:\t{encrypted_message!r}\nseed:\t\t{params['seed']!r}\nsize seed:\t{params['size_multiplier']}\nchar range:\t{params['char_range']}\nhex encoded:\t{params['hex']}" + + +def get_input() -> dict: + """\ + Set up argument parser and return it + + Returns: + dict: The parsed arguments in a dictionary form + """ + parser = ArgumentParser("Pseudo-Random Encryptor") + parser.add_argument("arg", type=str, help=help_messages["arg"]) + parser.add_argument("--hex", "-x", action="store_true", default=False, help=help_messages["hex"]) + parser.add_argument("--file", "-f", action="store_true", default=False, help=help_messages["file"]) + parser.add_argument("--decrypt", "-d", action="store_true", default=False, help=help_messages["decrypt"]) + parser.add_argument("--values-only", "-vo", action="store_true", default=False, help=help_messages["values only"]) + parser.add_argument("--seed", "-s", default=f"{Random().random()}"[2:], help=help_messages["seed"]) + parser.add_argument("--size", "-z", default=2, type=int, help=help_messages["size"]) + parser.add_argument("--char-range", "-r", type=int, default="128", help=help_messages["char range"]) + parser.add_argument("--output", "-o", type=str, default="", help=help_messages["output"]) + parser.add_argument("--garbage", "-g", default=None, help=help_messages["garbage seed"]) + args = parser.parse_args() + params = {k:v for k, v in args._get_kwargs()} + params["size_multiplier"] = args.size + del params["size"] + return params + + +def _extract_message(params: dict) -> str | int: + """\ + Extracts the original message to be encrypted/decrypted. + Reads the file if specified. + + Args: + params (dict): a dictionary of arguments + + Returns: + str|int: message (str) if it exists, otherwise -1 (int) + """ + if params["file"]: + try: + with open(params["arg"], "r") as f: + message = f.read() + except FileNotFoundError: + print("File does not exist") + return -1 + else: + message = params["arg"] + return message if message else -1 + + +def run(params: dict) -> None: + """\ + Main function + + Args: + params (dict): all the arguments passed to the program + + Returns: + None: Prints or writes output to the file + """ + message = _extract_message(params) + if message == -1: # file does not exist or empty file + print("No message to encrypt") + return + _validate(params) + + if params["decrypt"]: + message = message.encode().decode("unicode-escape").encode() + if params["hex"]: + message = bytes.fromhex(message.decode()) + output = decrypt(message, **params) + else: + params["encrypted_message"] = encrypt(message, **params) + + output = give_values(**params) if params["values_only"] else give_code(**params) + if params["output"]: + try: + with open(params["output"], "w") as f: + f.write(output) + except Exception as err: + print(f"Error occured while writing to file: {err}") + else: + print(output) + + +if __name__ == "__main__": + run(get_input())