diff --git a/Password-Manager/LICENSE b/Password-Manager/LICENSE new file mode 100644 index 0000000..120339d --- /dev/null +++ b/Password-Manager/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Anubhab Swain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Password-Manager/README.md b/Password-Manager/README.md new file mode 100644 index 0000000..20057dd --- /dev/null +++ b/Password-Manager/README.md @@ -0,0 +1,63 @@ +# Python Password Manager +>Note: This password manager was made as a project and is NOT intended for actual use. Please use more sophisticated and well-tested/trusted password managers to store sensitive data. + + +## AES Encryption + +The encryption method used in this program comes from the python library [PyCryptoDome](https://pypi.org/project/pycryptodome/). This program uses AES encryption methods to store sensitive data (in this case passwords) into a json file. + +## Hash Verification + To authenticate the user, they are prompted to create a master password (that is also used to decrypt data) which is then stored using a SHA256 Hash Function and is verified at login. Whenever the user is prompted to verify their master password, the password they enter is compared to the hash of the stored master password and access if granted if the two hashes match. + +## Requirements +- astroid (2.3.3) +- colorama (0.4.3) +- cursor (1.3.4) +- halo (0.0.28) +- isort (4.3.21) +- lazy-object-proxy (1.4.3) +- log-symbols (0.0.14) +- mccabe (0.6.1) +- pycryptodome (3.9.4) +- pylint (2.4.4) +- pyperclip (1.7.0) +- six (1.13.0) +- spinners (0.0.23) +- termcolor (1.1.0) +- typed-ast (1.4.0) +- wrapt (1.11.2) + +#### To be executed in the terminal: +```shell +pip install astroid +pip install colorama +pip install cursor +pip install halo +pip install isort +pip install lazy-object-proxy +pip install log-symbols +pip install mccabe +pip install pycryptodome +pip install pylint +pip install pyperclip +pip install six +pip install spinners +pip install termcolor +pip install typed-ast +pip install wrapt +``` + +## Vulnerability +As mentioned at the top, this was made as a project and not intended for actual use. Below I demonstrate what any expert hacker can accomplish by exploiting a vulnerability. Just kidding, anyone can do this. Since the files are stored locally, they can easily be deleted without needing to enter any credentials and consequently all stored passwords are gone along with other data. + +## Screenshots +[![Screenshot 1](https://github.com/anubhab-code/Password-Manager/blob/master/Screenshots/1.png "Screenshot 1")](https://github.com/anubhab-code/Password-Manager/blob/master/Screenshots/1.png "Screenshot 1") + +[![Screenshot 2](https://github.com/anubhab-code/Password-Manager/blob/master/Screenshots/2.png "Screenshot 2")](https://github.com/anubhab-code/Password-Manager/blob/master/Screenshots/2.png "Screenshot 2") + +[![Screenshot 3](https://github.com/anubhab-code/Password-Manager/blob/master/Screenshots/3.png "Screenshot 3")](https://github.com/anubhab-code/Password-Manager/blob/master/Screenshots/3.png "Screenshot 3") + +[![Screenshot 4](https://github.com/anubhab-code/Password-Manager/blob/master/Screenshots/4.png "Screenshot 4")](https://github.com/anubhab-code/Password-Manager/blob/master/Screenshots/4.png "Screenshot 4") + +[![Screenshot 5](https://github.com/anubhab-code/Password-Manager/blob/master/Screenshots/5.png "Screenshot 5")](https://github.com/anubhab-code/Password-Manager/blob/master/Screenshots/5.png "Screenshot 5") + diff --git a/Password-Manager/Screenshots/1.png b/Password-Manager/Screenshots/1.png new file mode 100644 index 0000000..0cbaf41 Binary files /dev/null and b/Password-Manager/Screenshots/1.png differ diff --git a/Password-Manager/Screenshots/2.png b/Password-Manager/Screenshots/2.png new file mode 100644 index 0000000..20165d9 Binary files /dev/null and b/Password-Manager/Screenshots/2.png differ diff --git a/Password-Manager/Screenshots/3.png b/Password-Manager/Screenshots/3.png new file mode 100644 index 0000000..d1a7af4 Binary files /dev/null and b/Password-Manager/Screenshots/3.png differ diff --git a/Password-Manager/Screenshots/4.png b/Password-Manager/Screenshots/4.png new file mode 100644 index 0000000..0c00603 Binary files /dev/null and b/Password-Manager/Screenshots/4.png differ diff --git a/Password-Manager/Screenshots/5.png b/Password-Manager/Screenshots/5.png new file mode 100644 index 0000000..809506b Binary files /dev/null and b/Password-Manager/Screenshots/5.png differ diff --git a/Password-Manager/main.py b/Password-Manager/main.py new file mode 100644 index 0000000..dea1166 --- /dev/null +++ b/Password-Manager/main.py @@ -0,0 +1,69 @@ +import os +import json +import sys +import getpass + +from os.path import isfile +from hashlib import sha256 +from termcolor import colored +from halo import Halo + +from modules.encryption import DataManip +from modules.exceptions import UserExits, PasswordFileDoesNotExist +from modules.menu import Manager + +def exit_program(): + print(colored("Exiting...", "yellow")) + sys.exit() + +def start(obj: DataManip): + if os.path.isfile("db/masterpassword.json"): + with open("db/masterpassword.json", 'r') as jsondata: + jfile = json.load(jsondata) + + stoyellow_master_pass = jfile["Master"] # load the saved hashed password + master_password = getpass.getpass("Enter Your Master Password: ") + + # compare the two hashes of inputted password and stoyellow + spinner = Halo(text=colored("Unlocking", "green"), color="green", spinner=obj.dots_) + if sha256(master_password.encode("utf-8")).hexdigest() == stoyellow_master_pass: + print(colored(f"{obj.checkmark_} Thank you! Choose an option below:", "green")) + # create instance of Manager class + menu = Manager(obj, "db/passwords.json", "db/masterpassword.json", master_password) + + try: + menu.begin() + except UserExits: + exit_program() + except PasswordFileDoesNotExist: + print(colored(f"{obj.x_mark_} DB not found. Try adding a password {obj.x_mark_}", "yellow")) + else: + print(colored(f"{obj.x_mark_} Master password is incorrect {obj.x_mark_}", "yellow")) + return start(obj) + + else: # First time running program: create a master password + try: + os.mkdir("db/") + except FileExistsError: + pass + + print(colored("To start, we'll have you create a master password. Be careful not to lose it as it is unrecoverable.", "green")) + master_password = getpass.getpass("Create a master password for the program: ") + second_input = getpass.getpass("Verify your master pasword: ") + + if master_password == second_input: + spinner = Halo(text=colored("initializing base...", "green"), color="green", spinner=obj.dots_) + hash_master = sha256(master_password.encode("utf-8")).hexdigest() + jfile = {"Master": {}} + jfile["Master"] = hash_master + with open("db/masterpassword.json", 'w') as jsondata: + json.dump(jfile, jsondata, sort_keys=True, indent=4) + spinner.stop() + print(colored(f"{obj.checkmark_} Thank you! Restart the program and enter your master password to begin.", "green")) + else: + print(colored(f"{obj.x_mark_} Passwords do not match. Please try again {obj.x_mark_}", "yellow")) + return start(obj) + +if __name__ == "__main__": + obj = DataManip() + start(obj) \ No newline at end of file diff --git a/Password-Manager/modules/__pycache__/__init__.cpython-37.pyc b/Password-Manager/modules/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000..7fe4db9 Binary files /dev/null and b/Password-Manager/modules/__pycache__/__init__.cpython-37.pyc differ diff --git a/Password-Manager/modules/__pycache__/encryption.cpython-37.pyc b/Password-Manager/modules/__pycache__/encryption.cpython-37.pyc new file mode 100644 index 0000000..0604d8e Binary files /dev/null and b/Password-Manager/modules/__pycache__/encryption.cpython-37.pyc differ diff --git a/Password-Manager/modules/__pycache__/encryption.cpython-38.pyc b/Password-Manager/modules/__pycache__/encryption.cpython-38.pyc new file mode 100644 index 0000000..f2ef682 Binary files /dev/null and b/Password-Manager/modules/__pycache__/encryption.cpython-38.pyc differ diff --git a/Password-Manager/modules/__pycache__/exceptions.cpython-37.pyc b/Password-Manager/modules/__pycache__/exceptions.cpython-37.pyc new file mode 100644 index 0000000..549a834 Binary files /dev/null and b/Password-Manager/modules/__pycache__/exceptions.cpython-37.pyc differ diff --git a/Password-Manager/modules/__pycache__/exceptions.cpython-38.pyc b/Password-Manager/modules/__pycache__/exceptions.cpython-38.pyc new file mode 100644 index 0000000..c227f88 Binary files /dev/null and b/Password-Manager/modules/__pycache__/exceptions.cpython-38.pyc differ diff --git a/Password-Manager/modules/__pycache__/menu.cpython-37.pyc b/Password-Manager/modules/__pycache__/menu.cpython-37.pyc new file mode 100644 index 0000000..ed789be Binary files /dev/null and b/Password-Manager/modules/__pycache__/menu.cpython-37.pyc differ diff --git a/Password-Manager/modules/__pycache__/menu.cpython-38.pyc b/Password-Manager/modules/__pycache__/menu.cpython-38.pyc new file mode 100644 index 0000000..04ced06 Binary files /dev/null and b/Password-Manager/modules/__pycache__/menu.cpython-38.pyc differ diff --git a/Password-Manager/modules/encryption.py b/Password-Manager/modules/encryption.py new file mode 100644 index 0000000..d6e7a56 --- /dev/null +++ b/Password-Manager/modules/encryption.py @@ -0,0 +1,275 @@ +import json +import string +import os +import random + +from Crypto.Cipher import AES +from halo import Halo +from termcolor import colored + +from modules.exceptions import * + +class DataManip: + def __init__(self): + self.dots_ = {"interval": 80, "frames": ["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"]} + self.checkmark_ = "\u2713" + self.x_mark_ = "\u2717" + self.specialChar_ = "!@#$%^&*()-_" + + def __save_password(self, filename, data, nonce, website): + """Saves password to DB + + Arguments: + filename {str} -- DB to save to + data {str} -- password that will be saved + nonce {hexadecimal} -- converted from byte type to hexadecimal as byte type is not supported in JSON + website {str} -- name of the website for the given password + """ + + spinner = Halo(text=colored("Saving", "green"), spinner=self.dots_, color="green") + spinner.start() + if os.path.isfile(filename): + try: + with open(filename, 'r') as jsondata: + jfile = json.load(jsondata) + jfile[website]["nonce"] = nonce + jfile[website]["password"] = data + with open(filename, 'w') as jsondata: + json.dump(jfile, jsondata, sort_keys=True, indent=4) + except KeyError: + with open(filename, 'r') as jsondata: + jfile = json.load(jsondata) + jfile[website] = {} + jfile[website]["nonce"] = nonce + jfile[website]["password"] = data + with open(filename, 'w') as jsondata: + json.dump(jfile, jsondata, sort_keys=True, indent=4) + else: # initialize the file in case it doesn't exist off the start + jfile = {website: {}} + jfile[website]["nonce"] = nonce + jfile[website]["password"] = data + with open(filename, 'w') as jsondata: + json.dump(jfile, jsondata, sort_keys=True, indent=4) + spinner.stop() + print(colored(f"{self.checkmark_} Saved successfully. Thank you!", "green")) + + + + def encrypt_data(self, filename, data, master_pass, website): + """Encrypt and save the data to a file using master password as the key + + Arguments: + filename {str} + data {str} -- password to save + master_pass {str} + website {str} -- website to store password + """ + + """Concatenated extra characters in the case that the master password + is less than 16 characters. However, this isn't a big safety trade off + as the full length master password is hashed and checked for.""" + concatenated_master = master_pass + "================" + + key = concatenated_master[:16].encode("utf-8") + + cipher = AES.new(key, AES.MODE_EAX) + + """A value that must never be reused for any other encryption done with + this key saved alongside encrypted password. Converted to hexadecimal + to be saved in DB. Later converted back to bytes to decode data""" + nonce = cipher.nonce.hex() + + data_to_encrypt = data.encode("utf-8") + # again, bytes is invalid data for JSON so we convert it + encrypted_data = cipher.encrypt(data_to_encrypt).hex() + + self.__save_password(filename, encrypted_data, nonce, website) + + def decrypt_data(self, master_pass, website, filename): + """Return a decrypted password as a string. + + Arguments: + master_pass {str} -- key + website {str} -- The password being returned is from this website + filename {str} -- database in which the password is stored. + + Raises: + PasswordNotFound: Password is not located in DB + PasswordFileDoesNotExist: The db is not initiated + + Returns: + str -- decrypted password + """ + + if os.path.isfile(filename): + try: + with open(filename, 'r') as jdata: + jfile = json.load(jdata) + nonce = bytes.fromhex(jfile[website]["nonce"]) + password = bytes.fromhex(jfile[website]["password"]) + except KeyError: + raise PasswordNotFound + else: + raise PasswordFileDoesNotExist + # add extra characters and take first 16 to make sure key is right. + formatted_master_pass = master_pass + "================" + master_pass_encoded = formatted_master_pass[:16].encode("utf-8") + cipher = AES.new(master_pass_encoded, AES.MODE_EAX, nonce = nonce) + plaintext_password = cipher.decrypt(password).decode("utf-8") + + return plaintext_password + + def generate_password(self): + """Generates a complex password + + Raises: + UserExits: user types "exit" in length + EmptyField: user leaves length field empty + PasswordNotLongEnough: raised when user enters a length below 8 + + Returns: + str -- complex password + """ + + password = [] + length = input("Enter Length for Password (At least 8): ") + + if length.lower().strip() == "exit": + raise UserExits + elif length.strip() == "": + raise EmptyField + elif int(length) < 8: + raise PasswordNotLongEnough + else: + # generating a password + spinner = Halo(text=colored("Generating Password", "green"), spinner=self.dots_, color="green") + spinner.start() + for i in range(0, int(length)): + #choose character from one of the lists randomly + password.append(random.choice(random.choice([string.ascii_lowercase, string.ascii_uppercase, string.digits, self.specialChar_]))) + + finalPass = "".join(password) + spinner.stop() + + return finalPass + + def list_passwords(self, filename): + """Loads a list of websites in DB + + Arguments: + filename {str} -- DB file + + Raises: + PasswordFileIsEmpty: No Passwords stored in DB + PasswordFileDoesNotExist: Password File Not found + + Returns: + str -- List of Passwords + """ + + if os.path.isfile(filename): + with open(filename, 'r') as jsondata: + pass_list = json.load(jsondata) + + passwords_lst = "" + for i in pass_list: + passwords_lst += "--{}\n".format(i) + + if passwords_lst == "": + raise PasswordFileIsEmpty + else: + return passwords_lst + else: + raise PasswordFileDoesNotExist + + def delete_db(self, filename, stored_master, entered_master): + """Delete DB/Password file & contents + + Arguments: + filename {str} -- DB/File to delete + stored_master {str} -- Stored master password in DB + entered_master {str} -- user-entered master password to authenticate + + Raises: + MasterPasswordIncorrect: Entered password does not match stored password + PasswordFileDoesNotExist: No file/db to delete + """ + if os.path.isfile(filename): + if stored_master == entered_master: + # first clear the data + spinner = Halo(text=colored("Deleting all password data...", "red"), spinner=self.dots_, color="red") + jfile = {} + with open(filename, 'w') as jdata: + json.dump(jfile, jdata) + # then delete the file + os.remove(filename) + spinner.stop() + else: + raise MasterPasswordIncorrect + else: + raise PasswordFileDoesNotExist + + def delete_password(self, filename, website): + """Deletes a single password from DB + + Arguments: + filename {str} -- Password file/DB + website {str} -- Password to delete + + Raises: + PasswordNotFound: No password for given website + PasswordFileDoesNotExist: No password file/DB + """ + + if os.path.isfile(filename): + with open(filename, 'r') as jdata: + jfile = json.load(jdata) + + try: + jfile.pop(website) + with open("db/passwords.json", 'w') as jdata: + json.dump(jfile, jdata, sort_keys=True, indent=4) + except KeyError: + raise PasswordNotFound + else: + raise PasswordFileDoesNotExist + + def delete_all_data(self, filename, master_file, stored_master, entered_master): + """Deletes ALL data including master password and passwords stored + + Arguments: + filename {str} -- Password db/file + master_file {str} -- Where masterpassword is stored + stored_master {str} -- The master password that is stored + entered_master {str} -- User-entered master password. Used to verify + + Raises: + MasterPasswordIncorrect: Passwords do not match + """ + + if os.path.isfile(master_file) and os.path.isfile(filename): # both files exist + if stored_master == entered_master: + spinner = Halo(text=colored("Deleting all data...", "red"), spinner=self.dots_, color="red") + # clear data + jfile = {} + with open(master_file, 'w') as jdata: + json.dump(jfile, jdata) + with open(filename, 'w') as jdata: + json.dump(jfile, jdata) + # delete file + os.remove(filename) + os.remove(master_file) + spinner.stop() + else: + raise MasterPasswordIncorrect + elif os.path.isfile(master_file) and not os.path.isfile(filename): # only master password exists + if stored_master == entered_master: + spinner = Halo(text=colored("Deleting all data...", "red"), spinner=self.dots_, color="red") + # clear data + jfile = {} + with open(master_file, 'w') as jdata: + json.dump(jfile, jdata) + os.remove(master_file) + spinner.stop() + else: + raise MasterPasswordIncorrect diff --git a/Password-Manager/modules/exceptions.py b/Password-Manager/modules/exceptions.py new file mode 100644 index 0000000..368420e --- /dev/null +++ b/Password-Manager/modules/exceptions.py @@ -0,0 +1,20 @@ +class PasswordNotFound(Exception): + pass + +class PasswordNotLongEnough(Exception): + pass + +class UserExits(Exception): + pass + +class PasswordFileDoesNotExist(Exception): + pass + +class EmptyField(Exception): + pass + +class PasswordFileIsEmpty(Exception): + pass + +class MasterPasswordIncorrect(Exception): + pass diff --git a/Password-Manager/modules/menu.py b/Password-Manager/modules/menu.py new file mode 100644 index 0000000..83e93df --- /dev/null +++ b/Password-Manager/modules/menu.py @@ -0,0 +1,345 @@ +import sys +import getpass +import pyperclip + +from termcolor import colored +from halo import Halo + +from modules.encryption import DataManip +from modules.exceptions import * + +class Manager: + """ + Arguments: + obj {DataManip} + filename {str} + master_pass {str} + """ + def __init__(self, obj: DataManip, filename: str, master_file: str, master_pass: str): + self.obj_ = obj + self.filename_ = filename + self.master_file_ = master_file + self.master_pass_ = master_pass + + def begin(self): + try: + # NOTE: fully tested already + choice = self.menu_prompt() + except UserExits: + raise UserExits + + if choice == '4': # User Exits + raise UserExits + + if choice == '1': # add or update a password + # NOTE: fully tested already + try: + self.update_db() + return self.begin() + except UserExits: + raise UserExits + + elif choice == '2': # look up a stored password + # NOTE: fully tested already + try: + string = self.load_password() + website = string.split(':')[0] + password = string.split(':')[1] + print(colored(f"Password for {website}: {password}", "yellow")) + + copy_to_clipboard = input("Copy password to clipboard? (Y/N): ").strip() + if copy_to_clipboard == "exit": + raise UserExits + elif copy_to_clipboard == 'y': + try: + pyperclip.copy(password) + print(colored(f"{self.obj_.checkmark_} Password copied to clipboard", "green")) + except pyperclip.PyperclipException: + print(colored(f"{self.obj_.x_mark_} If you see this message on Linux use `sudo apt-get install xsel` for copying to work. {self.obj_.x_mark_}", "red")) + else: + pass + + return self.begin() + except UserExits: + raise UserExits + except PasswordFileDoesNotExist: + print(colored(f"{self.obj_.x_mark_} DB not found. Try adding a password {self.obj_.x_mark_}", "red")) + return self.begin() + + elif choice == '3': # Delete a single password + # NOTE: fully tested already + try: + return self.delete_password() + except UserExits: + raise UserExits + + elif choice == '5': # Delete DB of Passwords + # NOTE: fully tested already + try: + self.delete_db(self.master_pass_) + except MasterPasswordIncorrect: + print(colored(f"{self.obj_.x_mark_} Master password is incorrect {self.obj_.x_mark_}", "red")) + return self.delete_db(self.master_pass_) + except UserExits: + raise UserExits + + elif choice == '6': # delete ALL data + # NOTE: fully tested already + try: + self.delete_all_data(self.master_pass_) + except MasterPasswordIncorrect: + print(colored(f"{self.obj_.x_mark_} Master password is incorrect {self.obj_.x_mark_}", "red")) + return self.delete_all_data(self.master_pass_) + except UserExits: + raise UserExits + + + + + def menu_prompt(self): + """Asks user for a choice from Menu + + Raises: + UserExits: User exits on choice prompt + + Returns: + str -- Users choice + """ + + print(colored("\n\t*Enter 'exit' at any point to exit.*\n", "magenta")) + print(colored("1) Add/Update a password", "blue")) + print(colored("2) Look up a stored password", "blue")) + print(colored("3) Delete a password", "blue")) + print(colored("4) Exit program", "blue")) + print(colored("5) Erase all passwords", "red")) + print(colored("6) Delete all data including Master Password", "red")) + + choice = input("Enter a choice: ") + + if choice == "": + return self.menu_prompt() # recursive call + elif choice == "exit": + raise UserExits + else: + return choice.strip() + + def __return_generated_password(self, website): + """Returns a generated password + + Arguments: + website {str} -- website for password + + Raises: + UserExits: User exits on loop prompt + + Returns: + str -- A randomly generated password + """ + + try: + generated_pass = self.obj_.generate_password() + print(colored(generated_pass, "yellow")) + + loop = input("Generate a new password? (Y/N): ") + if loop.lower().strip() == "exit": + raise UserExits + elif (loop.lower().strip() == 'y') or (loop.strip() == "") : + return self.__return_generated_password(website) # recursive call + elif loop.lower().strip() == 'n': + return generated_pass + except (PasswordNotLongEnough, EmptyField): + print(colored("Password length invalid.", "red")) + return self.__return_generated_password(website) + except UserExits: + print(colored("Exiting...", "red")) + sys.exit() + + + def update_db(self): # option 1 on main.py + """Add or update a password in the DB + + Raises: + UserExits: User enters exit at website prompt or generate prompt + """ + try: + self.list_passwords() + except PasswordFileIsEmpty: + pass + except PasswordFileDoesNotExist: + print(colored(f"--There are no passwords stored.--", "yellow")) + + + website = input("Enter the website for which you want to store a password (ex. google.com): ") + if website.lower() == "": + #raise EmptyField + self.update_db() + elif website.lower().strip() == "exit": + raise UserExits + else: + gen_question = input("Do you want to generate a password for {} ? (Y/N): ".format(website)) + if gen_question.strip() == "": + #raise EmptyField + self.update_db() + elif gen_question.lower().strip() == "exit": + raise UserExits + elif gen_question.lower().strip() == 'n': # user wants to manually enter a password + password = input("Enter a password for {}: ".format(website)) + if password.lower().strip() == "exit": + raise UserExits + else: + self.obj_.encrypt_data(self.filename_, password, self.master_pass_, website) + + elif gen_question.lower().strip() == 'y': + password = self.__return_generated_password(website) + self.obj_.encrypt_data("db/passwords.json", password, self.master_pass_, website) + + def load_password(self): + """Loads a string of websites stored and asks user to enter a + website, then decrypts password for entered website + + Raises: + PasswordFileDoesNotExist: DB is not initialized + UserExits: User enters exit on website prompt + + Returns: + str -- string formatted in website:password + """ + try: + self.list_passwords() + except PasswordFileIsEmpty: + return self.begin() + + + + + website = input("Enter website for the password you want to retrieve: ") + + if website.lower().strip() == "exit": + raise UserExits + elif website.strip() == "": + return self.load_password() + else: + try: + plaintext = self.obj_.decrypt_data(self.master_pass_, website, self.filename_) + except PasswordNotFound: + print(colored(f"{self.obj_.x_mark_} Password for {website} not found {self.obj_.x_mark_}", "red")) + return self.load_password() + except PasswordFileDoesNotExist: + print(colored(f"{self.obj_.x_mark_} DB not found. Try adding a password {self.obj_.x_mark_}", "red")) + return self.begin() + + # see https://pypi.org/project/clipboard/ for copying to clipboard + final_str = f"{website}:{plaintext}" + + return final_str + + def delete_db(self, stored_master): + """Menu Prompt to Delete DB/Passwords + + Arguments: + stored_master {str} -- Used to authenticate, compared with inputted master password + + Raises: + PasswordFileDoesNotExist: Password file not initialized + """ + + confirmation = input("Are you sure you want to delete the password file? (Y/N)") + if confirmation.lower().strip() == 'y': + entered_master = getpass.getpass("Enter your master password to delete all stored passwords: ") + if entered_master.lower().strip() == "exit": + raise UserExits + else: + try: + self.obj_.delete_db(self.filename_, stored_master, entered_master) + print(colored(f"{self.obj_.checkmark_} Password Data Deleted successfully. {self.obj_.checkmark_}", "green")) + return self.begin() + except MasterPasswordIncorrect: + raise MasterPasswordIncorrect + except PasswordFileDoesNotExist: + print(colored(f"{self.obj_.x_mark_} DB not found. Try adding a password {self.obj_.x_mark_}", "red")) + return self.begin() + elif confirmation.lower().strip() == 'n': + print(colored("Cancelling...", "red")) + return self.begin() + elif confirmation.lower().strip() == "exit": + raise UserExits + elif confirmation.strip() == "": + return self.delete_db(stored_master) + + def list_passwords(self): + """Lists all websites stored in DB + """ + + print(colored("Current Passwords Stored:", "yellow")) + spinner = Halo(text=colored("Loading Passwords", "yellow"), color="yellow", spinner=self.obj_.dots_) + + try: + lst_of_passwords = self.obj_.list_passwords(self.filename_) + spinner.stop() + print(colored(lst_of_passwords, "yellow")) + except PasswordFileIsEmpty: + lst_of_passwords = "--There are no passwords stored.--" + spinner.stop() + print(colored(lst_of_passwords, "yellow")) + raise PasswordFileIsEmpty + except PasswordFileDoesNotExist: + raise PasswordFileDoesNotExist + + def delete_password(self): + """Deletes a single password from DB + + Raises: + UserExits: User exits + """ + try: + self.list_passwords() + except PasswordFileIsEmpty: + return self.begin() + + website = input("What website do you want to delete? (ex. google.com): ").strip() + + if website == "exit": + raise UserExits + elif website == "": + return self.delete_password() + else: + try: + self.obj_.delete_password(self.filename_, website) + print(colored(f"{self.obj_.checkmark_} Data for {website} deleted successfully.", "green")) + return self.begin() + except PasswordNotFound: + print(colored(f"{self.obj_.x_mark_} {website} not in DB {self.obj_.x_mark_}", "red")) + return self.delete_password() + except PasswordFileDoesNotExist: + print(colored(f"{self.obj_.x_mark_} DB not found. Try adding a password {self.obj_.x_mark_}", "red")) + return self.begin() + + def delete_all_data(self, stored_master): + """Deletes ALL data including master password and passwords stored. Asks for user confirmation. + + Arguments: + stored_master {str} -- Master password that is stored + + Raises: + UserExits: User enters exit + MasterPasswordIncorrect: Master Passwords do not match + """ + confirmation = input("Are you sure you want to delete all data? (Y/N)") + if confirmation.lower().strip() == 'y': + entered_master = getpass.getpass("Enter your master password to delete all stored passwords: ") + if entered_master.lower().strip() == "exit": + raise UserExits + else: + try: + self.obj_.delete_all_data(self.filename_, self.master_file_, stored_master, entered_master) + print(colored(f"{self.obj_.checkmark_} All Data Deleted successfully. {self.obj_.checkmark_}", "green")) + sys.exit() + except MasterPasswordIncorrect: + raise MasterPasswordIncorrect + elif confirmation.lower().strip() == 'n': + print(colored("Cancelling...", "red")) + return self.begin() + elif confirmation.lower().strip() == "exit": + raise UserExits + elif confirmation.strip() == "": + return self.delete_all_data(stored_master) \ No newline at end of file diff --git a/Password-Manager/requirements.txt b/Password-Manager/requirements.txt new file mode 100644 index 0000000..c50fe78 --- /dev/null +++ b/Password-Manager/requirements.txt @@ -0,0 +1,18 @@ +Required Modules : + +astroid==2.3.3 +colorama==0.4.3 +cursor==1.3.4 +halo==0.0.28 +isort==4.3.21 +lazy-object-proxy==1.4.3 +log-symbols==0.0.14 +mccabe==0.6.1 +pycryptodome==3.9.4 +pylint==2.4.4 +pyperclip==1.7.0 +six==1.13.0 +spinners==0.0.23 +termcolor==1.1.0 +typed-ast==1.4.0 +wrapt==1.11.2