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.
1 Answer 1
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.
-
\$\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– Beginner2023年04月25日 13:55:01 +00:00Commented Apr 25, 2023 at 13:55
Explore related questions
See similar questions with these tags.