3
\$\begingroup\$

22/12/2025

This is the almost final version of my encryption/decryption function that I started to make two weeks ago.

The first version was actually an big giant error, but with the received answers I understood the basic that was needed to be at least "not bad".

I remade everything, with all I understood, knew and could. I really think it is not bad now.

This code is missing only one feature I'm trying to apply, which is a text scrambler at the final result text. When I finish with it, I'll add the link to the final version on the last line of this question, so if you want it (I don't really recommend for REAL sensitive information, but you can always encrypt more), just click the link and copy the function.

This was my first attempt. Take a look at the answers, because they are really good, specially the first, in terms of encryption.

Goal for the code

Encrypts and decrypts an string, keeping it same length (or almost).

CODE

"""
The module basic_py (this file) has some functions that
you can import or copy/paste on you'r project.

*If you are going to copy/paste, pay atention on code
to not forget to copy other depended functions.*

The functions are:

    (...other functions on the same file i made...)

    X_to_decimal():
        Decodes an number coded in X
        (type of encoding, like Hexadecimal) to decimal.

    get_index():
        Gets the index of an object on an given list,
        the first time it is found.

    basic_py_enc_engine():
        First step for basic_py_encrypter() and basic_py_decrypter().
        *You don't need to call it.*
        Just scrambles an list of characters
        based on password hash with sha3-512.

    basic_py_encrypter():
        Encrypts text based on key using sha3-512.

    basic_py_decrypter():
        Decrypts texts encrypted with basic_py_encrypter().

    basic_ui_bpy_enc():
        Is an basic UI for the basic_py_encrypter/decrypter.

Made with the help from @J_H, @toolic, @Chris, @Kate on SO on first attempt.
"""
from hashlib import sha3_512
def x_to_decimal(Digit: str, Lista: list|str = "0123456789abcdef") -> int:
    """Converts Digit coded by Lista
    (the 'allphabet' of the encoded digit) to decimal digits.

    Lista defalt is Hexadecimal"""
    output = 0
    for value in range(len(Digit)):
        for index, char in enumerate(Lista):
            if Digit[((value - len(Digit)) * -1 - 1)] == char:
                output += index * (len(Lista) ** (value))
    return output
def get_index(Char: str|list, Lista: list|tuple|dict|str) -> int:
    """Gets index of character on given list.
    If Char isn't found on Lista, returns None

    :Char: Is the character to be searched.
    (If whole str is desired, put into an list),
    If is an list, gives the index of the first common iten.

    :Lista: Is the place to be searched on."""
    for _, character in enumerate(Char):
        for index, char in enumerate(Lista):
            if character == char:
                return index
def basic_py_enc_engine(Key: str, Security_word: str = "") -> tuple[str, str]:
    """First code before basic_py_encrypter() and basic_py_decrypter()

    :Don't need to call: The basic_py_encr. and basic_py_decr. allreay call it."""

    #   1º block some base variables
    characters = '¨0123456789abcçdefghijklmnopqrstuvwxyzáàãâéèêíìîóòõôúùûABCÇDEFGHIJKLMNOPQRSTUVWXYZÁÀÃÂÉÈÊÍÌÎÓÒÕÔÚÙÛ !@#$%&*()-_=+´"`[]}{~^<>|/,.;:?¹²³£¢¬§ªº°₢'
    # 1 "False" for each character, used later on to verify if every character was used
    base_verification_list = [False] * len(characters)
    full_key = Key + Security_word

    #   2º block hash code
    key_hash = x_to_decimal(sha3_512(full_key.encode()).hexdigest())
    for _ in range(3): # key_hash needs to be at least ~400 len to scramble every character
        key_hash = key_hash ** 2
    if key_hash >= 10 ** 4000:
        # set key_hash to have at max 1.5k characters if key_hash > 4300 char
        time = 0
        max_time = 10**2
        aceptable_lengh = 10**1500
        max_characters = 10**4200
        while key_hash > aceptable_lengh:
            key_hash = key_hash // 10 ** 20
            time += 1
            if time >= max_time and key_hash < max_characters:
                # it tests if key_hash allready is usable if its taking too long
                break

    #   3º block allphabet scrambler code
    hash_word = str(key_hash) if len(str(key_hash)) % 3 == 0 else str(key_hash) + "0" if len(str(key_hash)) % 3 == 2 else str(key_hash) + "00"
    # The hash_word needs to be divisible by 3.
    hash_indexes_3len = [hash_word[index: index + 3] for index in range(0, len(hash_word), 3)]
    scrambled_allphabet = ""
    verification = 0
    for _, char in enumerate(hash_indexes_3len):
        # prevents from crash if char is bigger than 141
        while int(char) > len(characters):
            char = str(int(char) - len(characters))
        for index, _ in enumerate(characters):
            if not base_verification_list[index] and int(char) == index:
                    base_verification_list[index] = True
                    verification += 1
                    scrambled_allphabet += "".join(characters[index])
        if verification == len(characters):
            break
    # Test if allphabet scrambler code have scrambled every character
    if verification != len(characters):
        for idx in range(len(characters)):
            if not base_verification_list[idx]:
                scrambled_allphabet += "".join(characters[idx])

    return characters, scrambled_allphabet, 
def basic_py_encrypter(Key: str, Text: str, Security_word: str = "") -> str:
    """BEFORE saving the encrypted output, be sure that/test if the Key can decrypt it right.

    :Key: (String) Is the password to use to encrypt.
    :Text: (String) Is the text to be encrypted.
    :Security_word: (String) Is the (2º password) word that will be added on encrypted text and used on decryption to test. (If let blank, there wont be an test on decryption).

    :OUTPUT: String
    :VERSION: v0.95 //Working on text scrambler//"""

    characters, scrambled_allphabet = basic_py_enc_engine(Key, Security_word)
    full_text = Security_word + Text
    output_text = ""
    for _, character in enumerate(full_text):
        character = '"' if character == "'" else character
        letter = characters[get_index(character, scrambled_allphabet)]
        output_text +=  letter
    return output_text
def basic_py_decrypter(Key: str, Text: str, Security_word: str = "") -> str:
    """:Key: (String) Is the password used to encrypt.
    :Text: (String) Is the text to be decrypted.
    :Security_word: (String) Is the (2º password) word that you gave or not when encrypting. (Let blank if not used).
    :OUTPUT: String
    :VERSION: v0.95 //Working on text scrambler//"""

    characters, scrambled_allphabet = basic_py_enc_engine(Key, Security_word)
    # Turns True or False depending on Security_word
    Security = True if Security_word != "" else False
    output_text = ""
    test = ""
    if Security:
        security_test_list = [Text[0: len(Security_word)][index] for index in range(len(Security_word))]
        for _, index in enumerate(security_test_list):
            test += scrambled_allphabet[get_index(index, characters)]
        if Security_word != test:
            return
    text_in_list = [Text[index] for index in range(len(Text))]
    verification = len(Security_word)
    while verification > 0:
        text_in_list.pop(0)
        verification -= 1
    for _, index in enumerate(text_in_list):
        output_text += scrambled_allphabet[get_index(index, characters)]
    return output_text
def basic_ui_bpy_enc() -> str:
    """This is an basic User Interface in Comand Prompt for the basic_py_encrypter/decrypter().

    Returns 'Q' if "Quit" is chosen."""
    while True:
        start_choice = input("\nBasic_py_encrypter/decrypter.py V0.95\n[E]ncrypt\n[D]ecrypt\n[Q]uit\n\n> ").strip().lower()
        if start_choice == "q":
            print("\nBye!\n")
            return "Q"
        if start_choice != "e" and start_choice != "d":
            print("Invalid input.")
            continue
        break
    path_choice = "encrypt" if start_choice == "e" else "decrypt"
    text = input(f"\nYou'r text to {path_choice}:\n> ")
    key = input(f"\nYou'r key for {path_choice}:\n> ")
    security_word = input(f"\nSecurity word?{'' if start_choice == 'e' else ' (If used)'}\n(enter 'n' to leave blanck)\n> ")
    security_word = "" if security_word == "n" else security_word
    if start_choice == "e":
        return basic_py_encrypter(key, text, security_word)
    return basic_py_decrypter(key, text, security_word)
if __name__ == "__main__":
    # simple use of basic_ui_bpy_enc():
    hint = True
    print("""\nNotes:
    Note #1: Always choose a strong password, containing special characters,
    lowercase and uppercase letters and numbers.
    Note #2: Always use trusted libraries for creating password hashes.
    Note #3: Stop using weak hashing algorithms such as md5, sha1, sha256, etc.""")
    while True:
        test = basic_ui_bpy_enc()
        if test == "Q":
            break
        print(f"\nYou'r output:\n> {test}")
        if hint:
            print("...why don't you try encrypting it multiple times with different Keys for more security?...")
            hint = False
\$\endgroup\$

3 Answers 3

4
\$\begingroup\$

A few observations

  • Detailing all of the functions in the module's docstring feels a bit overdone. Each function should have a docstring, which they do. The module docstring should give us an overview of what the module is for.
  • You can use more newlines to make your code easier to read. Certainly between imports and functions. I tend to like newlines between if/else and try/except blocks; and loops.
  • Function parameter names being capitalized feels very unidiomatic. Review PEP 8.
def x_to_decimal(Digit: str, Lista: list|str = "0123456789abcdef") -> int:
    """Converts Digit coded by Lista
    (the 'allphabet' of the encoded digit) to decimal digits.

    Lista defalt is Hexadecimal"""
    output = 0
    for value in range(len(Digit)):
        for index, char in enumerate(Lista):
            if Digit[((value - len(Digit)) * -1 - 1)] == char:
                output += index * (len(Lista) ** (value))
    return output
  • range(len(Digit)) raises an immediate eyebrow.
  • You have extra parens: index * (len(Lista) ** (value)) can be: index * len(Lista) ** value
  • It looks like there's an opportunity here to sum a generator expression.
def get_index(Char: str|list, Lista: list|tuple|dict|str) -> int:
  • Using a type hint of list|tuple|dict|str looks like a complex way to say Iterable.
from collections import Iterable
    Security = True if Security_word != "" else False
  • The above is better as Security = Security_word != "". Just use the boolean value you have.
    while True:
        start_choice = input("\nBasic_py_encrypter/decrypter.py V0.95\n[E]ncrypt\n[D]ecrypt\n[Q]uit\n\n> ").strip().lower()
        if start_choice == "q":
            print("\nBye!\n")
            return "Q"
        if start_choice != "e" and start_choice != "d":
            print("Invalid input.")
            continue
        break
  • The control flow seems little bit too "clever."
  • You may wish to use a match construct.
    while True:
        start_choice = input("\nBasic_py_encrypter/decrypter.py V0.95\n[E]ncrypt\n[D]ecrypt\n[Q]uit\n\n> ").strip().lower()

        match start_choice:
            case "q":
                print("\nBye!\n")
                return "Q"
            case "e" | "d":
                break
            case _:
                print("Invalid input.")
  • You have some spelling errors.
        print(f"\nYou'r output:\n> {test}")
  • You have a pointless call to enumerate, since you're not using the index:
    for _, character in enumerate(full_text):
        character = '"' if character == "'" else character
        letter = characters[get_index(character, scrambled_allphabet)]
        output_text +=  letter
\$\endgroup\$
1
  • \$\begingroup\$ Yes, the extra parêntesis was a mistake; the Security part i genuinelly didnt thought; the "match" construct i never saw. I'll remember this to start using; in the enumerate part i could use an range(len(full_text)) to get the full_text[index], right? I though use enumerate would be faster \$\endgroup\$ Commented 12 hours ago
4
\$\begingroup\$

Chris made several points which I will add to or elaborate on.

Spelling

There are several other misspelled words:

aceptable
allphabet
allready
allreay
atention
blanck
defalt
lengh

I can call a spell-checker in my code editor. Perhaps yours has a similar capability.

Layout

The black program can be used to automatically add blank lines between your sections of code, in a consistent manner.

You may want to use the --line-length option to have it set lines of code to your desired length.

There are some lines that are too long, as in this docstring line:

:Security_word: (String) Is the (2º password) word that will be added on encrypted text and used on decryption to test. (If let blank, there wont be an test on decryption).

That should be split into multiple lines to help readability:

:Security_word: (String) Is the (2º password) word that will be added on encrypted text and used on decryption to test. 
                (If let blank, there wont be an test on decryption).

ASCII

Consider using the string library to generate strings such as:

characters = '¨0123456789abcçdefghijklmnopqrstuvwxyzáàãâéèêíìîóòõôúùûABCÇDEFGHIJKLMNOPQRSTUVWXYZÁÀÃÂÉÈÊÍÌÎÓÒÕÔÚÙÛ !@#$%&*()-_=+´"`[]}{~^<>|/,.;:?¹²³£¢¬§ªº°'

Functions ascii_letters and digits make the code more readable and leave no doubt that there aren't errors in that long string.

DRY

In the basic_py_enc_engine function, this expression is repeated several times:

len(characters)

You should be able to set it to a variable:

num_chars = len(characters)

This will also make the code more efficient since you only need to execute len once.

\$\endgroup\$
4
\$\begingroup\$

Docstrings and Type Hints

You are using these and that is great!

PEP 8 Style Guide

Read this document and apply its recommendations, in particular the naming of variables (your variables and arguments should use lower case) and leaving blank lines between import statements and function definitions. You also have some rather long lines that could be broken up.

Constant Values

Variables such as max_time, acceptable_length, max_characters, which are assigned constant values, should be constants named with upper case and defined outside of any loops or if statements, i.e. either at the top of function basic_py_enc_engine or as global variables (which are more visible to a reader) with suitable comments if they might be subject to change in the future. Constant values such as 10 ** 4000, 10 ** 1500`, etc. should be assigned to meaningful constant names with comments so we know what these values represent.

Function Simplification

You have multiple opportunities to improve function efficiency and clarity. You should go through your code and look for other places where you are either performing the same invariant calculation multiple times, calculating a value that is not used or performing a calculation that could be done less expensively. But here are a couple of examples:

Function x_to_decimal

If we had a truly optimizing compiler, then common sub-expression elimination and induction variable analysis (noting how a an expression varies in each iteration of a loop and replacing an expensive calculation with a cheaper one) would be performed:

  1. The expression ((value - len(Digit)) * -1 - 1) is invariant inside the inner loop and it should be "hoisted", i.e. calculated prior to entering the loop.
  2. The subexpression (value - len(Digit)) * -1 can be simplified to len(Digit) - 1.
  3. The expressions len(Digit) and len(Lista) should only be calculated once.
  4. Digit[((value - len(Digit)) * -1 - 1)] is an invariant within the inner loop and should be calculated prior to entering the loop.
  5. The variable named value really represents an index into Digit and should be renamed to something like idx.
  6. The inner loop has output += index * (len(Lista) ** (value)). As the inner loop is iterated, the expression index * (len(Lista) ** (value)) takes on values 0, len(Lista) ** (value), 2 * len(Lista) ** (value), etc. That is, the expression changes by the value len(Lista) ** (value) on each iteration. Multiplication can therefore be replaced by addition.
  7. Likewise, we can replace exponentiation by multiplication.

All of the above changes plus a couple of more optimizations result in:

def x_to_decimal(Digit: str, Lista: list|str = "0123456789abcdef") -> int:
    """Converts Digit coded by Lista
    (the 'allphabet' of the encoded digit) to decimal digits.

    Lista defalt is Hexadecimal"""

    lista_len = len(Lista)
    output = 0
    term_increment = 1

    for ch in Digit[::-1]:
        term = 0
        for char in Lista:
            if ch == char:
                output += term
            term += term_increment

        term_increment *= lista_len

    return output

Function get_index

You have:

    for _, character in enumerate(Char):

But if you are not using the index returned by enumerate, why are you even using enumerate? This is not the only function in which you do this.

Use Appropriate Data Structures and Pythonic Idioms

In function basic_py_decrypter you have:

    text_in_list = [Text[index] for index in range(len(Text))]

This could be more simply and efficiently done as:

    text_in_list = [ch for ch in Text]

But even better:

    text_in_list = list(Text)

But later on you execute:

    while verification > 0:
        text_in_list.pop(0)
        ...

Popping from the front of a list is more expensive than popping from the end. You should be using instead a deque:

from collections import deque
...
text_in_list = deque(Text)

Odds and Ends

You have:

        character = '"' if character == "'" else character

Why not just:

        if character == "'":
            character = '"'

You have:

    Security = True if Security_word != "" else False

This should be:

    Security = Security_word != ""

if __name__ == "__main__": Guard

You have a lot of code within this block. If this code is intended just to show the reader a possible scenario invoking the other functions, then this is fine. But if the code is really meant to always be executed, then better to move it into some function and have the "main guard" contain a single statement that invokes that function.

\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.