1
\$\begingroup\$

I am still just starting out with python and just finished learning about classes, with no further understanding on many python modules. With this in mind, this is one of the biggest/hardest things I have done.

The password manager creates a file with as many passwords as you want or one individual password to a file. It 'encrypts' the password with a randomly generated 'key'. This isn't going to be the best coded encrypter(scrambling is more of what is being done) and it is in no way ready for actual real world usage. Although, I am always looking to improve on myself and would appreciate in depth explanations on what I did wrong, what could be better implemented, and further additions to be made.

Module: v2_Password_Manager.py

#v.2 of Password Manager, more functionality and upgrades
import os
import random

ALPHABET = 'abcdefghijklmnopqrstuvxyzABCDEFGHIJKLMNOPQURSTUVWXYZ'
NUMERIC = '1234567890'
SYMBOLIC = ')(!@#$%^&*-_=+<>,.?:;'

def encrypting_password(password):
    password_charachters = [char for char in password]
    key = str(random.randint(111, 999))
    #each different key is one digit from the overall key generated
    alphabet_key, numeric_key, symbolic_key = int(key[0]), int(key[1]), int(key[2])
    encrypted_password = []

    for char in password_charachters:
        #for each charachter, gets new index using key corresponding to charachter's type
        if char in ALPHABET:
            alphabet_index = ALPHABET.index(char)
            new_char = alphabet_index + alphabet_key
            
            if new_char + ALPHABET.index(char) > len(ALPHABET) - 1:
                new_char = new_char - len(ALPHABET)
            encrypted_password.append(ALPHABET[new_char])

        elif char in NUMERIC:
            numeric_index = NUMERIC.index(char)
            new_char = numeric_index + numeric_key

            if new_char + NUMERIC.index(char) > len(NUMERIC) - 1:
                new_char = new_char - len(NUMERIC)
            encrypted_password.append(NUMERIC[new_char])
        
        elif char in SYMBOLIC:
            symbolic_index = SYMBOLIC.index(char)
            new_char = symbolic_index + symbolic_key

            if new_char + SYMBOLIC.index(char) > len(SYMBOLIC) - 1:
                new_char = new_char - len(SYMBOLIC)
            encrypted_password.append(SYMBOLIC[new_char])
    #list of password and key is returned for decrypting
    return [''.join(encrypted_password), key]

def decrypting_password(encrypted_password):
    decrypted_password = []
    alphabet_key, numeric_key, symbolic_key = int(encrypted_password[1][0]), int(encrypted_password[1][1]), int(encrypted_password[1][2])
    
    #simply reverses the original lists and uses the key of each char to get original position
    for char in encrypted_password[0]:
        if char in ALPHABET:
            alphabet_index = ALPHABET[::-1].index(char)
            new_char = alphabet_index + alphabet_key
            
            if new_char + ALPHABET[::-1].index(char) > len(ALPHABET) - 1:
                new_char = new_char - len(ALPHABET)
            decrypted_password.append(ALPHABET[::-1][new_char])

        elif char in NUMERIC:
            numeric_index = NUMERIC[::-1].index(char)
            new_char = numeric_index + numeric_key

            if new_char + NUMERIC[::-1].index(char) > len(NUMERIC) - 1:
                new_char = new_char - len(NUMERIC)
            decrypted_password.append(NUMERIC[::-1][new_char])
        
        elif char in SYMBOLIC:
            symbolic_index = SYMBOLIC[::-1].index(char)
            new_char = symbolic_index + symbolic_key

            if new_char + SYMBOLIC[::-1].index(char) > len(SYMBOLIC) - 1:
                new_char = new_char - len(SYMBOLIC)
            decrypted_password.append(SYMBOLIC[::-1][new_char])

    return ''.join(decrypted_password)

def generating_password():
    password = []

    #randomly defines how many of each charachter will be in a single password
    num_of_alphabets = random.randint(5, 7)
    num_of_numbers = random.randint(3, 5)
    num_of_symbols = random.randint(1, 3)
    
    for i in range(num_of_alphabets):
        password.append(random.choice(ALPHABET))
    for i in range(num_of_numbers):
        password.append(random.choice(NUMERIC))
    for i in range(num_of_symbols):
        password.append(random.choice(SYMBOLIC))

    random.shuffle(password)

    return ''.join(password)

def creating_file(name, password_amount = 1,  password = ''):
    #File name is required, password_amount determines how many passwords you want, password is 
    #optional to specify what password you want decrypted and in this case password_amount decrypts this password multiple times
    with open(f'{name}.txt', 'w+') as f:
        if password == '':
            for i in range(password_amount):
                i += 1
                encrypted_password = encrypting_password(generating_password())
                password = encrypted_password[0]
                key = encrypted_password[1] 
                f.write(f'\nPassword {i}: {password} || Key {i}: {key}')
        else:
            with open(f'{name}.txt', 'w+') as f:
                for i in range(password_amount):
                    i += 1
                    encrypted_password = encrypting_password(password)
                    f.write(f'\nPassword {i}: {encrypted_password[0]} || Key {i}: {encrypted_password[1]}')

def decrypting_file(file):
    with open(f'{file}.txt', 'r+') as f:
        #A whole bunch of reformatting to enable the stripping at the end of function
        list = f.readlines()
        list.pop(0)

        for line, i in zip(list, range(len(list))):
            i += 1
            password_info = line.split('||')
            password = password_info[0].replace(f'Password {i}: ', '').rstrip()
            key = password_info[1].rstrip('\n').replace(f' Key {i}: ', '')
            print(f'{i}. The password ({password}) was decrypted into: ({decrypting_password([password, key])})')

File: v2_Program.py

import os

from v2_Password_Manager import creating_file, decrypting_file, SYMBOLIC

def setting_mode():
    while True:
        mode = input('\n----------------------\n- Create Password[c] -\n----------------------\n-    Open File[o]    -\n----------------------\n-       Quit[q]      -\n----------------------\n      Input:')
        if mode in 'cCoOqQ':
            return mode
        else:
            print('\nInvalid input. Try Again.')

def initializing_create():
    
    naming_file = True
    setting_preference = True
    setting_password_amount = True
    password_amount = None

    while naming_file:
        file_name = input('\nName of file encrypted password will be saved to [No use of special charachters]: ')
        for char in file_name:
            if char == '_':
                pass
            elif char in SYMBOLIC:
                print('Invalid Charachter Detected')
                break
        naming_file = False

    while setting_preference:
        password = input('\nDo you want a specific password to be saved and encrypted to your device [type in your password if \'yes\', type \'no\' if not]: ')
        if password.lower() == 'no':
            while setting_password_amount:
                password_amount = input('\nHow many passwords do you want generated? ')
                if password_amount.isdigit():
                    password_amount = int(password_amount)
                    setting_preference = False
                    setting_password_amount = False
                else:
                    print('Not a number')
                    continue
        elif password.lower() == 'yes':
            password = input('\nWhat is your password? ')
            setting_preference = False
        else:
            continue

    if password_amount == None:
        creating_file(file_name,1 ,password)
    else:
        creating_file(file_name, password_amount)

def initializing_open():
    opening_file = True

    while opening_file:
        file_name = input('\nWhat file would you like to open? ')
        if os.path.exists(f'{file_name}.txt'):
            decrypting_file(file_name)
            opening_file = False
        else:
            print('This file doesn\'t exist. Try again.')
            continue
    pass

def executing_mode(mode):
    if mode in 'Cc':
        initializing_create()
    elif mode in 'Oo':
        initializing_open()
    else:
        print('\nThanks for your support! See you next time.')
        print('\nQuitting')
        quit()
    


running = True

if __name__ == '__main__':
    print('\nWelcome to Prototype Password Manager.\nHere you can create passwords which will be encrypted in a file saved to your device.\nFurther functionality will be added for user specific save files with another layer of password protection.\nEnjoy!')

    while running:
        mode = setting_mode()
        executing_mode(mode)

Help in running: If you would like to run it, copy the 'Module' code and put it in a file named 'v2_Password_Manager.py'. Then copy the 'File' code and put it in a python file named 'v2_Program.py' or whatever you want this time. Put both of these in the same directory and then run from terminal or whatever you have to run them.

Extra Info: The reason I named them v2 was because this was the second time I have tried this exact thing and I quit almost immediately. I make it a point to not delete any of my unfinished projects, so I can look back on them and see how much I have progressed.

OLD CODE

v1_Password_Manager.py

#Password Encryption

alphabets = '_abcdefghijklmnopqrstuvwxyz'
digits = '_1234567890@_'

def password_encrypter(password,key,digit_key):
    encrypted_password = []
    
    for char in password:
        if char.isdigit() or (char in '@_'):
            char_index = digits.index(char)
            char_index = char_index + digit_key

            while char_index > 12:
                char_index = abs(char_index - 12)
            else:
                encrypted_password.append(digits[char_index])
        else:
            letter_index = alphabets.index(char.lower())
            letter_index = letter_index + key

            while letter_index > 26:
                letter_index = abs(letter_index - 26)
            else:
                if char == char.upper():
                    encrypted_password.append(alphabets[letter_index].lower())
                else:
                    encrypted_password.append(alphabets[letter_index].upper())

    return [''.join(encrypted_password), key, digit_key]


def password_decrypter(encrypted_password, key, digit_key):
    password = []

    for char in encrypted_password:
        if char.isdigit() or char in '@_':
            digit_index = digits.index(char)
            digit_index = abs(digit_index-digit_key)
            print(digit_index)
            
            if digit_index > 11:
                while digit_index > 11:
                    digit_index -= 11
                    digit_index = abs(digit_index)
                else:
                    password.append(digits[digit_index])
            else:
                password.append(digits[digit_index])
        print(password)

encrypted = password_encrypter('567891', 2, 9)
print(encrypted)

#password_decrypter(encrypted[0], encrypted[1], encrypted[2])

Didn't finish this one

v1_Program.py

from v2_Password_Manager import creating_file, decrypting_file, SYMBOLIC

def running_program():
    # print('\nWelcome to Prototype Password Manager.\nHere you can create passwords which will be encrypted in a file saved to your device.\nFurther functionality will be added for user specific save files with another layer of password protection.\nEnjoy!')
    # while True:
    #     initial_loop = True
    #     mode = input('\n----------------------\n- Create Password[c] -\n----------------------\n-    Open File[o]    -\n----------------------\n       Input:')
        if mode in 'cC':
            while initial_loop:
                file_name = input('\nName of file encrypted password will be saved to [No use of special charachters]: ')
                for char in file_name:
                    if char in SYMBOLIC:
                        print('Invalid Charachter Detected')
                        break
                        #issue with looped content in general. Refactor code
                        
                password = input('\nDo you want a specific password to be saved and encrypted to your device [type in your password if yes, type \'NO\' if not]: ')
                if password == 'NO':
                    while True:
                        password_amount = input('\nHow many passwords do you want generated? ')
                        password_amount = int(password_amount)
                        if password_amount.isdigit():
                            initial_loop = False
                        else:
                            print('Not a number')
                else:
                    initial_loop = False
            initial_loop = False

        elif mode in 'oO':
            pass
        # else:
        #     print('\nInvalid input. Try Again.')
        #     continue

running_program()

This was my first attempt of the 'Program' file.

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Don't roll your own crypto

At least, you recognize yourself that your code "is in no way ready for actual real world usage". I get that this is an exercise intended for teaching yourself programming, but researching what third-party libraries best fit your use case and how to use them is a big part of programming in practice.

Cryptography is a very complex topic with very serious implications if done wrong, so it is the perfect candidate for teaching yourself how to work with other people's code.

The Python standard library offers some cryptography-related modules that are worth checking out, but as far as I know, there is no cryptographically strong encryption library, which means you're going to have to also do some research and find a good third-party module for that purpose. Maybe sslcrypto could fit the bill?

As far as your current implementation goes, I'm very far from a cryptography expert, but here are the issues I can immediately spot:

Use a cryptographically strong RNG

The random pseudo-RNG is not suited for crypto applications (as noted in the module documentation), and you should use secrets instead.

Predictable password generation

Your generating_password function generates passwords of hard-coded length with a constant count of each character type, reducing the search space for attackers.

At the very least, the function should take the output length and alphabet as arguments and pick characters randomly through the alphabet. This also allows the user to match the various real-world requirements imposed on password choice.

Example adapted from the secrets module documentation:

import secrets


def generate_password(alphabet: str, length: int) -> str:
    return ''.join(secrets.choice(alphabet) for _ in range(length))

Very weak encryption

Your encryption algorithm is basically a Caesar cipher, which wasn't hard to break even back in medieval times, let alone to anyone with access to a computer.

Storing the key in plain text

When using the password manager, the program retrieves the "encrypted" passwords along with the key in a plain text file. At no point a user password is used, making it even more trivial to break the cipher.

Other remarks

Naming

Function names should use the affirmative tense, as it is more natural to read: encrypt_password, create_file, set_mode, etc.

Use docstrings

You document some of your functions and modules using regular comments. Use doctrings instead, allowing accessing them though the help function or the IDE.

Limit line length

You have lines up to more than 200 characters long, requiring to scroll horizontally to read the code. The PEP 8 Python style guide recommends limiting the line length to 80 characters.

Multi-line strings

Your program often use multiple lines of text for console output, encoded as single strings containing \n characters. This is hard to read and should be replaced by triple-quoted strings (allowing line breaks) or multiple calls to print. An (arguably better) alternative would be to assign these strings to constants, avoiding to have presentation data in the middle of the code.

# don't:
mode = input('\n----------------------\n- Create Password[c] -\n----------------------\n-    Open File[o]    -\n----------------------\n-       Quit[q]      -\n----------------------\n      Input:')

# do:
mode = input('''
----------------------
- Create Password[c] -
----------------------
-    Open File[o]    -
----------------------
-       Quit[q]      -
----------------------
      Input:''')

# or:
print('----------------------')
print('- Create Password[c] -')
print('----------------------')
print('-    Open File[o]    -')
print('----------------------')
print('-       Quit[q]      -')
print('----------------------')
mode = input('      Input:')

# better:
MENU = '''
----------------------
- Create Password[c] -
----------------------
-    Open File[o]    -
----------------------
-       Quit[q]      -
----------------------'''
PROMPT = 'Input:'


print(MENU)
mode = input(PROMPT)

Versioning

Your versioning using file names adds unwanted noise to the names (most visible in the import statement) and is hard to maintain. It's never too early to use a version managers (ie git), or at the very least keep your older version in another folder and leave version numbers out of the module names.

Unused import

Remove the unused import os statement from the v2_Password_Manager file.

Use string constants

Your ALPHABET and NUMERIC constants can be replaced by constants from the string module: string.ascii_letters and string.digits respectively.

Opening a file twice

In the creating_file method, the file {name}.txt is opened twice in nested with statements in the case a password is provided. I'm surprised this doesn't cause any bugs, but in any case the second with statement can and should be removed.

\$\endgroup\$
1
  • \$\begingroup\$ Thank you for the constructive criticism. I will get around to learning modules in depth. I also did not at all notice the double with statements. \$\endgroup\$
    – Beginner
    Commented Apr 25, 2023 at 13:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.