ChamplainTechJournals/python-csi160/password-manager/password-manager.py
2025-04-19 23:42:08 -04:00

389 lines
No EOL
14 KiB
Python

import pickle
import sys
import random
import string
# The password list - We start with it populated for testing purposes
entries = {'yahoo': {'username': 'johndoe', 'password': 'cus#u%S tu', 'url': 'https://www.yahoo.com'},
'google': {'username': 'johndoe', 'password': '`q$$( #tABCD^ %fu#*W t', 'url': 'https://www.google.com'}}
# The password file name to store the data to
password_file_name = "PasswordFile.pickle"
# The encryption key for the caesar cypher
encryption_key = 16
menu_text = """
What would you like to do:
1. Open password file
2. Add an entry
3. Lookup an entry
4. Save password file
5. Quit program
6. Print dictionary for testing
7. Delete an entry
8. Edit an entry
Please enter a number (1-8)"""
def password_encrypt(unencrypted_message, key):
"""Returns an encrypted message using a caesar cypher
:param unencryptedMessage (string)
:param key (int) The offset to be used for the caesar cypher
:return (string) The encrypted message
"""
result_string = ''
min_limit = 32
max_limit = 126
for character in unencrypted_message:
value = ord(character) - min_limit + key
value = value % (max_limit - min_limit + 1)
value = value + min_limit
result_string = result_string + chr(value)
return result_string
def password_decrypt(encrypted_message, key):
"""Returns a decrypted message.
:param encrypted_message (string):
:param key (int) The offset that was used to encrypt the message
:return (string): The decrypted message
"""
return password_encrypt(encrypted_message, -key)
def load_password_file():
"""Loads a password file. The file must be in the same directory as the .py file
"""
global entries, encryption_key
try:
entries, encryption_key = pickle.load(open(password_file_name, "rb"))
print(f"Password file '{password_file_name}' loaded successfully!")
except FileNotFoundError:
print(f"File '{password_file_name}' not found. Starting with an empty dictionary.")
entries = {}
except Exception as e:
print(f"Error loading file: {e}")
def save_password_file():
"""Saves a password file. The file will be created if it doesn't exist.
"""
try:
pickle.dump((entries, encryption_key), open(password_file_name, "wb"))
print(f"Password file '{password_file_name}' saved successfully!")
except Exception as e:
print(f"Error saving file: {e}")
def generate_random_password(length=12):
"""Generates a random password that meets complexity requirements.
The generated password will:
- Be at least 8 characters long (default 12)
- Contain at least one uppercase letter
- Contain at least one lowercase letter
- Contain at least one digit
- Contain at least one special character
:param length (int): Length of the password to generate (minimum 8)
:return (string): A randomly generated password meeting all requirements
"""
if length < 8:
length = 8 # Enforce minimum length
# Define character sets
uppercase_letters = string.ascii_uppercase
lowercase_letters = string.ascii_lowercase
digits = string.digits
special_chars = "!@#$%^&*()-_=+[]{}|;:,.<>?"
# Ensure we have at least one of each required character type
password = [
random.choice(uppercase_letters),
random.choice(lowercase_letters),
random.choice(digits),
random.choice(special_chars)
]
# Fill the rest with random characters from all allowed sets
all_chars = uppercase_letters + lowercase_letters + digits + special_chars
for _ in range(length - 4):
password.append(random.choice(all_chars))
# Shuffle the password
random.shuffle(password)
# Convert list to string
return ''.join(password)
def check_password_complexity(password):
"""Checks if a password meets complexity requirements.
Requirements:
- At least 8 characters long
- Contains at least one uppercase letter
- Contains at least one lowercase letter
- Contains at least one digit
- Contains at least one special character
:param password (string): The password to check
:return (tuple): (bool, string) - True if the password meets all requirements,
False otherwise, and a message explaining the result
"""
# Check length
if len(password) < 8:
return False, "Password must be at least 8 characters long."
# Check for uppercase letter
if not any(char.isupper() for char in password):
return False, "Password must contain at least one uppercase letter."
# Check for lowercase letter
if not any(char.islower() for char in password):
return False, "Password must contain at least one lowercase letter."
# Check for digit
if not any(char.isdigit() for char in password):
return False, "Password must contain at least one digit."
# Check for special character
special_chars = "!@#$%^&*()-_+=<>?/[]{}|\\~`"
if not any(char in special_chars for char in password):
return False, "Password must contain at least one special character."
return True, "Password meets all complexity requirements."
def add_entry():
"""Adds an entry with an entry title, username, password and url
Includes all user interface interactions to get the necessary information from the user
"""
try:
entry_title = input("Enter entry title: ")
username = input("Enter username: ")
# Explain password requirements
print("\nPassword requirements:")
print("- At least 8 characters long")
print("- Contains at least one uppercase letter")
print("- Contains at least one lowercase letter")
print("- Contains at least one digit")
print("- Contains at least one special character\n")
# Ask if user wants to generate a random password
gen_password = input("Would you like to generate a random password? (y/n): ")
if gen_password.lower() == 'y':
# Ask for desired password length
try:
length = int(input("Enter desired password length (minimum 8, default 12): ") or "12")
if length < 8:
print("Password length must be at least 8. Setting to 8.")
length = 8
except ValueError:
print("Invalid input. Using default length of 12.")
length = 12
# Generate the password
password = generate_random_password(length)
print(f"Generated password: {password}")
is_valid = True
else:
# Get and validate manual password
print("Enter your password manually:")
while True:
password = input("Enter password: ")
is_valid, message = check_password_complexity(password)
if is_valid:
print(message)
break
else:
print(message)
print("Please try again with a stronger password.")
url = input("Enter URL: ")
# Encrypt the password
encrypted_password = password_encrypt(password, encryption_key)
# Add the new entry to the entries dictionary
entries[entry_title] = {
'username': username,
'password': encrypted_password,
'url': url
}
print(f"Entry '{entry_title}' added successfully!")
except Exception as e:
print(f"Error adding entry: {e}")
def print_entry():
"""Asks the user for the name of the entry and prints all related information in a pretty format.
Includes all information about an entry.
"""
try:
print("Which entry do you want to lookup the information for?")
for key in entries:
print(key)
entry = input('Enter entry name: ')
# Check if the entry exists
if entry in entries:
entry_data = entries[entry]
# Decrypt the password
decrypted_password = password_decrypt(entry_data['password'], encryption_key)
# Print the entry information
print("\n--- Entry Information ---")
print(f"Entry: {entry}")
print(f"Username: {entry_data['username']}")
print(f"Password: {decrypted_password}")
print(f"URL: {entry_data['url']}")
print("------------------------\n")
else:
print(f"Entry '{entry}' not found!")
except KeyError:
print(f"Error: The entry does not exist or has incomplete data.")
except Exception as e:
print(f"Error looking up entry: {e}")
def delete_entry():
"""Deletes an entry from the password manager.
Asks the user for the entry name to delete and removes it if it exists.
This is additional feature #1.
"""
try:
print("Which entry do you want to delete?")
for key in entries:
print(key)
entry = input('Enter entry name: ')
# Check if the entry exists
if entry in entries:
# Ask for confirmation
confirm = input(f"Are you sure you want to delete '{entry}'? (y/n): ")
if confirm.lower() == 'y':
# Remove the entry
del entries[entry]
print(f"Entry '{entry}' deleted successfully!")
else:
print("Deletion cancelled.")
else:
print(f"Entry '{entry}' not found!")
except Exception as e:
print(f"Error deleting entry: {e}")
def edit_entry():
"""Allows the user to edit an existing entry in the password manager.
The user can update the username, password, and/or URL.
This is additional feature #2.
"""
try:
print("Which entry do you want to edit?")
for key in entries:
print(key)
entry = input('Enter entry name: ')
# Check if the entry exists
if entry in entries:
entry_data = entries[entry]
print(f"\nEditing entry: {entry}")
# Get current values for reference
current_username = entry_data['username']
current_url = entry_data['url']
# Update username
new_username = input(f"Enter new username (current: {current_username}) or press Enter to keep current: ")
if new_username:
entry_data['username'] = new_username
# Update password
update_password = input("Do you want to update the password? (y/n): ")
if update_password.lower() == 'y':
# Explain password requirements
print("\nPassword requirements:")
print("- At least 8 characters long")
print("- Contains at least one uppercase letter")
print("- Contains at least one lowercase letter")
print("- Contains at least one digit")
print("- Contains at least one special character (!@#$%^&*()-_+=<>?/[]{}|\\~`)\n")
# Ask if user wants to generate a random password
gen_password = input("Would you like to generate a random password? (y/n): ")
if gen_password.lower() == 'y':
# Ask for desired password length
try:
length = int(input("Enter desired password length (minimum 8, default 12): ") or "12")
if length < 8:
print("Password length must be at least 8. Setting to 8.")
length = 8
except ValueError:
print("Invalid input. Using default length of 12.")
length = 12
# Generate the password
new_password = generate_random_password(length)
print(f"Generated password: {new_password}")
# Encrypt the new password
entry_data['password'] = password_encrypt(new_password, encryption_key)
else:
# Get and validate new password manually
while True:
new_password = input("Enter new password: ")
is_valid, message = check_password_complexity(new_password)
if is_valid:
print(message)
# Encrypt the new password
entry_data['password'] = password_encrypt(new_password, encryption_key)
break
else:
print(message)
print("Please try again with a stronger password.")
# Update URL
new_url = input(f"Enter new URL (current: {current_url}) or press Enter to keep current: ")
if new_url:
entry_data['url'] = new_url
print(f"Entry '{entry}' updated successfully!")
else:
print(f"Entry '{entry}' not found!")
except Exception as e:
print(f"Error editing entry: {e}")
def end_program():
"""Exits the program.
"""
sys.exit()
def print_dictionary():
"""Prints the current entries dictionary.
For testing purposes only.
"""
print(entries)
# Menu dictionary mapping user choices to functions
menu_dict = {'1': load_password_file,
'2': add_entry,
'3': print_entry,
'4': save_password_file,
'5': end_program,
'6': print_dictionary,
'7': delete_entry,
'8': edit_entry}
# Main program loop
while True:
try:
user_choice = input(menu_text)
if user_choice in menu_dict and menu_dict[user_choice]:
menu_dict[user_choice]()
else:
print('Not a valid choice')
except Exception as e:
print(f"An error occurred: {e}")