├── README.md └── project-files ├── db_connect.py ├── main.py ├── master_password.py ├── master_password_hash_generator.py ├── password_generator.py └── sql_statements.py /README.md: -------------------------------------------------------------------------------- 1 | # Python-SQL CLI Password Manager 2 | 3 | ## Overview 4 | A Password Manager to securely manage and store passwords with URL, username, and passwords fields. A Master Password is used to authenticate into the manager "Vault", where all other passwords are stored. 5 | 6 | ## Infrastructure 7 | A docker container with PostgresSQL is used to store passwords. To setup the a new Docker container, with PostgreSQL installed, refer to this article: https://dev.to/andre347/how-to-easily-create-a-postgres-database-in-docker-4moj - super easy setup! Follow the outlined steps within the article. 8 | 9 | ## Requried Libraries 10 | - Hashlib 11 | - Cryptodome 12 | - pbkdf2 13 | - psycopg2 14 | - os 15 | - getpass 16 | - sys 17 | 18 | ## Setup 19 | 20 | ### Step 1: Clone Project and Project Files 21 | 22 | ```git clone https://github.com/collinsmc23/python-sql-password-manager``` 23 | 24 | ### Step 2: Generate a Master Password Hash 25 | 26 | There are "two factors". of authentiation included within the program. There are two options. You have to make sure the second_FA_location variable is either commented out or has a string inside the main.py program. If you do want to have a second factor of authentication, then you need to include hash the master_password_hash and the second_FA. 27 | 28 | Enter the master password hash inside the master_password_hash variable inside the master_password.py program. 29 | 30 | ### Step 3: Connect to Docker Container 31 | Enter in correct username, database name, and password inside the db_connect.py file. 32 | 33 | ### Step 4: Run Main.py 34 | Run main.py with all files inside the same directory. 35 | 36 | ```main.py [ARGUMENT] [OPTIONS}``` 37 | 38 | Example to add Password: ```main.py -a https://cybercademy.org gcollins``` 39 | 40 | Enter the one of the following paramters: 41 | 42 | `-a or --add [WEBSITE URL] [USERNAME]`: Automatically generates a random 20 character string for password. 43 | `-q or --query [WEBSITE URL]`: Look up field by website URL. 44 | `-l or --list`: List all stored fields in password vault. 45 | `-d or --delete [WEBSITE URL]`: Delete a field by website URL. 46 | `-ap or --add_password [WEBSITE_URL] [USERNAME] [PASSWORD]`: Enter in a URL, username, and custom password. 47 | `-uurl or --update_url [NEW_URL] [OLD_URL]`: Update the URL with new URL to currently stored URL. 48 | `-uuname or --update_username [URL] [NEW_USERNAME]`: Update username of stored URL. 49 | `-upasswd or --update_password [URL] [NEW_PASSWORD]`: Update new password by stored URL. 50 | -------------------------------------------------------------------------------- /project-files/db_connect.py: -------------------------------------------------------------------------------- 1 | import psycopg2 2 | 3 | def connection_db(): 4 | # Enter password under ******** field. 5 | connection = psycopg2.connect("dbname=Vault-DB user=postgres password=********") 6 | return connection 7 | -------------------------------------------------------------------------------- /project-files/main.py: -------------------------------------------------------------------------------- 1 | import password_generator 2 | import sql_statements 3 | import db_connect 4 | import psycopg2 5 | import argparse 6 | import master_password 7 | import getpass 8 | import sys 9 | import hashlib 10 | 11 | def main(): 12 | 13 | my_parser = argparse.ArgumentParser(description="Password Manager Vault: Create, Add, and Delete URL, Usernames, and Passwords", 14 | usage="[options]") 15 | 16 | master_password_input = getpass.getpass("Master Password: ").encode() 17 | 18 | second_FA_location = "Dee Boo Dah".encode() 19 | 20 | master_password_hash = hashlib.sha256(master_password_input + second_FA_location).hexdigest() 21 | 22 | if master_password.query_master_pwd(master_password_input, second_FA_location) is True: 23 | 24 | connection = db_connect.connection_db() 25 | 26 | print("\nSucessfully Authenticated.\n") 27 | 28 | else: 29 | print("Failed to authenticate into server. Run the program again.") 30 | sys.exit() 31 | 32 | 33 | my_parser.add_argument("-a", "--add", type=str, nargs=2, help="Add new entry", metavar=("[URL]", "[USERNAME]")) 34 | my_parser.add_argument("-q", "--query", type=str, nargs = 1, help="Look up entry by URL", metavar=("[URL]")) 35 | my_parser.add_argument("-l", "--list", action="store_true", help="List all entries in password vault") 36 | my_parser.add_argument("-d", "--delete", type=str, nargs=1, help="Delete entry by URL", metavar=("[URL]")) 37 | my_parser.add_argument("-ap", "--add_password", type=str, nargs=3, help="Add manual password", metavar=("[URL]", "[USERNAME]", "[PASSWORD]")) 38 | my_parser.add_argument("-uurl", "--update_url", type=str, nargs=2, help="Update a URL", metavar=("[NEW_URL]", "[OLD_URL]")) 39 | my_parser.add_argument("-uuname", "--update_username", type=str, nargs=2, help="Update a username in account", metavar=("[URL]", "[NEW_USERNAME]")) 40 | my_parser.add_argument("-upasswd", "--update_password", type=str, nargs=2, help="Update a password in account", metavar=("[URL]", "[NEW_PASSWORD]")) 41 | 42 | 43 | args = my_parser.parse_args() 44 | 45 | cursor = connection.cursor() 46 | 47 | connection.commit() 48 | 49 | if args.add: 50 | URL = args.add[0] 51 | username = args.add[1] 52 | password = password_generator.password_gen(20) 53 | password_official = master_password.encrypt_password(password, master_password_hash) 54 | cursor.execute(sql.insert_db_row(), (URL, username, password_official)) 55 | print("Record Added:" + "\n URL: {0}, Username: {1}, Password: {2} (Plaintext Password)".format(URL, username, password)) 56 | print("Record Added:" + "\n URL: {0}, Username: {1}, Password: {2} (Encrypted Ciphertext to be Stored)".format(URL, username, password_official)) 57 | 58 | if args.query: 59 | URL = args.query[0] 60 | cursor.execute(sql.select_db_entry(), (URL, )) 61 | record = cursor.fetchone() 62 | password_field = record[2] 63 | decrypt_password = master_password.decrypt_password(password_field, master_password_hash) 64 | 65 | if bool(record): 66 | print("Record: " + "\n URL: {0}, Username: {1}, Password: {2}".format(record[0], record[1], decrypt_password.decode('utf-8'))) 67 | print("Record With Encrypted Password: " + "\n URL: {0}, Username: {1}, Password: {2}".format(record[0], record[1], record[2])) 68 | else: 69 | print("Could not find record matching the value of \'%s\'" % (URL)) 70 | 71 | if args.delete: 72 | URL = args.delete[0] 73 | sql_delete_query = """Delete from Vault where URL = %s""" 74 | cursor.execute(sql_delete_query, (URL, )) 75 | 76 | if args.add_password: 77 | URL = args.add_password[0] 78 | username = args.add_password[1] 79 | password = args.add_password[2] 80 | password_official = master_password.encrypt_password(password, master_password_hash) 81 | cursor.execute(sql.insert_db_row(), (URL, username, password_official)) 82 | print("Record added with custom password.") 83 | 84 | if args.update_url: 85 | new_URL = args.update_url[0] 86 | old_URL = args.update_url[1] 87 | cursor.execute(sql.update_db_url(), (new_URL, old_URL, )) 88 | 89 | if args.update_username: 90 | new_username = args.update_username[0] 91 | URL = args.update_username[1] 92 | cursor.execute(sql.update_db_usrname(), (new_username, URL )) 93 | 94 | if args.update_password: 95 | print("Please type in old password: ") 96 | new_password = args.update_password[0] 97 | URL = args.update_password[1] 98 | cursor.execute(sql.update_db_passwd(), (new_password, URL )) 99 | 100 | if args.list: 101 | cursor.execute("SELECT * from Vault") 102 | record = cursor.fetchall() 103 | for i in range(len(record)): 104 | entry = record[i] 105 | for j in range(len(entry)): 106 | titles = ["URL: ", "Username: ", "Password: "] 107 | if titles[j] == "Password: ": 108 | bytes_row = entry[j] 109 | password = master_password.decrypt_password(bytes_row, master_password_hash) 110 | print("Password: " + str(password.decode('utf-8'))) 111 | else: 112 | print(titles[j] + entry[j]) 113 | 114 | 115 | print( "----------") 116 | 117 | connection.commit() 118 | 119 | cursor.close() 120 | 121 | main() 122 | 123 | -------------------------------------------------------------------------------- /project-files/master_password.py: -------------------------------------------------------------------------------- 1 | from hashlib import sha256 2 | from Cryptodome.Cipher import AES 3 | from pbkdf2 import PBKDF2 4 | import hashlib 5 | from base64 import b64encode, b64decode 6 | 7 | # Enter salt here in ******* field. Enter binary string. 8 | salt = b'********' 9 | 10 | 11 | def query_master_pwd(master_password, second_FA_location): 12 | 13 | # Enter password hash in ******** field. Use PBKDF2 and Salt from above. Use master_password_hash_generator.py to generate a master password hash. 14 | master_password_hash = "********" 15 | 16 | compile_factor_together = hashlib.sha256(master_password + second_FA_location).hexdigest() 17 | 18 | if compile_factor_together == master_password_hash: 19 | return True 20 | 21 | def encrypt_password(password_to_encrypt, master_password_hash): 22 | 23 | key = PBKDF2(str(master_password_hash), salt).read(32) 24 | 25 | data_convert = str.encode(password_to_encrypt) 26 | 27 | cipher = AES.new(key, AES.MODE_EAX) 28 | 29 | nonce = cipher.nonce 30 | 31 | ciphertext, tag = cipher.encrypt_and_digest(data_convert) 32 | 33 | add_nonce = ciphertext + nonce 34 | 35 | encoded_ciphertext = b64encode(add_nonce).decode() 36 | 37 | return encoded_ciphertext 38 | 39 | def decrypt_password(password_to_decrypt, master_password_hash): 40 | 41 | if len(password_to_decrypt) % 4: 42 | 43 | password_to_decrypt += '=' * (4 - len(password_to_decrypt) % 4) 44 | 45 | convert = b64decode(password_to_decrypt) 46 | 47 | key = PBKDF2(str(master_password_hash), salt).read(32) 48 | 49 | nonce = convert[-16:] 50 | 51 | cipher = AES.new(key, AES.MODE_EAX, nonce=nonce) 52 | 53 | plaintext = cipher.decrypt(convert[:-16]) 54 | 55 | return plaintext 56 | 57 | 58 | -------------------------------------------------------------------------------- /project-files/master_password_hash_generator.py: -------------------------------------------------------------------------------- 1 | from hashlib import sha256 2 | import hashlib 3 | 4 | def master_password_gen(): 5 | 6 | master_password = input("Enter your password: ").encode() 7 | 8 | compile_factor_together = hashlib.sha256(master_password).hexdigest() 9 | 10 | print("Master Password: " + str(compile_factor_together)) 11 | 12 | master_password_gen() 13 | -------------------------------------------------------------------------------- /project-files/password_generator.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | import string 3 | 4 | def password_gen(password_length): 5 | 6 | characters = string.ascii_letters + string.digits 7 | 8 | secure_password = ''.join(secrets.choice(characters) for i in range(password_length)) 9 | 10 | return secure_password 11 | -------------------------------------------------------------------------------- /project-files/sql_statements.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def insert_db_row(): 4 | insert_query = """INSERT INTO Vault (URL, USRNAME, PASSWD) VALUES (%s, %s,%s)""" 5 | return insert_query 6 | 7 | 8 | def delete_db_row(): 9 | sql_delete_query = """Delete from Vault where URL = %s""" 10 | return sql_delete_query 11 | 12 | def update_db_url(): 13 | update_query_url = """UPDATE Vault SET url = %s WHERE url = %s""" 14 | return update_query_url 15 | 16 | def update_db_usrname(): 17 | update_query_usrname = """UPDATE Vault SET usrname = %s WHERE url = %s""" 18 | return update_query_usrname 19 | 20 | def update_db_passwd(): 21 | update_query_passwd = """UPDATE Vault SET passwd = %s WHERE url = %s""" 22 | return update_query_passwd 23 | 24 | def select_db_entry(): 25 | select_query = """SELECT * from vault where url = %s""" 26 | return select_query 27 | 28 | def update_db(): 29 | update_db = """UPDATE Vault SET passwd = %s""" 30 | return update_db 31 | --------------------------------------------------------------------------------