├── .gitignore ├── .gitattributes ├── SALT.txt ├── banner.png ├── VERIFIER.txt ├── requirements.txt ├── pm_db.mmf ├── LICENSE ├── god_key_hasher.py ├── .github └── workflows │ └── codeql-analysis.yml ├── README.md └── pm_db.py /.gitignore: -------------------------------------------------------------------------------- 1 | caesarcipher.py 2 | __pycache__/god_key_hasher.cpython-37.pyc 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /SALT.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkMcKinney/DIY-Password-Manager/HEAD/SALT.txt -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkMcKinney/DIY-Password-Manager/HEAD/banner.png -------------------------------------------------------------------------------- /VERIFIER.txt: -------------------------------------------------------------------------------- 1 | gAAAAABhJR5CTqq891PejbM4EY7GdzoakaghJPuZpdJs-ejov3xaC6i9fZ_Z7tliAUMzXTESvMvNk4zXcJ6M4UaJp8HIOyTjU_erkoy1Myr8yKRQScDcMsk= -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | inputimeout==1.0.4 2 | keyboard==0.13.5 3 | cryptography==3.3.1 4 | pyperclip==1.8.2 5 | argon2_cffi==20.1.0 6 | argon2==0.1.10 7 | secrets==1.0.2 8 | -------------------------------------------------------------------------------- /pm_db.mmf: -------------------------------------------------------------------------------- 1 | gAAAAABhJR5rK_p5lFKXd7DMBtgi8kcjr2hVn6YIdVy_lhn-uECeuMkPL6ZinUQj6tJOT58isvSvyM3lv0CciPYW_gZjnO9QNEU6mTTThELVo_nR-r0oa0VFDTocKNe_XkLDVDZ0eLwm9LGpdBEO3SuZe3NKo4bTL76tMH3evzS7VCrgfvMZGUJPge9dOxJ-kqKP5Xg4sKu4hGrHgq_p_4JJq8PJAlZOkZg8iLdxIB3_KfQe3yQJkzAJcOEnf20mKgXjBN2BPFikQXWL4sH7BjCY9jjZ5VordeC99GwSKPEikIUlup2keJwt-QpSQncMvqHc-JqLi1DZwnqavwDMCalOo74CJNtZv7QJZZAMB2CyKdsye7PVC1xpU37V7YDzjTN-ED1KOOLynkLF0q9M0VzrU7Ho2zmoh-lkevWnVhlHZoTSqpCbGllvi7SGDLk3IQjLcm3FIAL9AYnRNnXVSvPDqc3C2CRbNB0WidqGdkaEaiGWK1vV35FlmIorM88HOXpkKm9s4kwSLmtaIhfN13LsIdjM4N_leEqH2z5phs-uYyFJaXWfIYt0sPjf6A5RwN28j7GM55l1BlliV9cMA9dQsebSUwLqosnFxzGVGb7I2csURO7sPit_lw_KUUdpLCEttP3jVm6mnfc2rNdWIbuvDhyZVyAnRN0JK4xFixNDH418CS-7cEpa-Ush5YEcNc6wUrZeCGyDO2wrKl04cMrhYJuc1rs__k4uHB2If0MKkpR7VwZeU24v2sSxWDd77k6NbEK-8-4F -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 MarkMcKinney 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /god_key_hasher.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import os 3 | from cryptography.hazmat.primitives.kdf.scrypt import Scrypt 4 | from cryptography.fernet import Fernet 5 | import random 6 | import getpass 7 | import argon2 8 | from argon2 import PasswordHasher 9 | 10 | # OPERATION FUNCTIONS 11 | 12 | def encrypt_data(input, hashed_pass): 13 | message = input.encode() 14 | f = Fernet(hashed_pass) 15 | encrypted = f.encrypt(message) 16 | return (encrypted) 17 | 18 | def decrypt_data(input, hashed_pass): 19 | f = Fernet(hashed_pass) 20 | decrypted = f.decrypt(input) 21 | return (decrypted) 22 | 23 | def argon2Hash(input): 24 | 25 | ph = PasswordHasher(time_cost=32, memory_cost=8589935000, parallelism=8, hash_len=256, salt_len=32, encoding='utf-8', 26 | type=argon2.Type.ID) 27 | hash = ph.hash(input.encode()) 28 | 29 | return hash 30 | 31 | def vaultSetup(): 32 | password_provided = getpass.getpass("What would you like your master password to be? ") 33 | password = password_provided.encode() # Convert to type bytes 34 | salt = os.urandom(32) 35 | kdf = Scrypt( 36 | salt=salt, 37 | length=32, 38 | n=2**14, 39 | r=8, 40 | p=1, 41 | ) 42 | hashed_entered_pass = base64.urlsafe_b64encode(kdf.derive(password)) # Can only use kdf once 43 | 44 | file = open("SALT.txt", "wb") 45 | file.write(salt) 46 | file.close() 47 | del salt 48 | 49 | file = open("VERIFIER.txt", "wb") 50 | file.write(encrypt_data("entered_master_correct",hashed_entered_pass)) 51 | file.close() 52 | 53 | file = open("pm_db.mmf", "w+") 54 | file.write(str(encrypt_data("{}",hashed_entered_pass).decode('utf-8'))) 55 | file.close() 56 | del hashed_entered_pass 57 | 58 | input("Your password vault was created. Access it using the pm_db.py file. Press ENTER to continue to login...") 59 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '27 7 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![DIY Password Manager Screenshot](banner.png) 2 | 3 | # Password Manager 4 | I built this to strengthen my cryptography skills and knowledge of data types in Python. This Password Manager doesn't keep your master password. Instead, valid passwords are determined if it decrypts a known string correctly. Typically, the salt and verifier hashes would be kept in a secure database. However, for educational purposes, I've kept them in text files. 5 | 6 | I'd love to hear from you about where I could improve it! You can also follow me on [Twitter](https://twitter.com/MarkDMcKinney) to keep updated on this project's (and other projects') progress. 7 | 8 | **EDIT (8/10/2021):** After posting this on [r/Python](https://www.reddit.com/r/Python/comments/p22p35/i_made_a_password_manager_for_the_terminal_let_me/), one Redditor asked what my threat model was. Initially, I had no idea what they were referring to, but after some research, I think this was the mental version I had of a threat model. I wanted to accomplish two things: 9 | 10 | 1. Make sure that all logins were encrypted, but editable. 11 | 2. That the master key wasn’t stored in any form. 12 | 13 | This was done by encrypting JSON that contained logins, having all hashes and salts created with as much randomness as possible, and having an encrypted verifier string that the program knew what the decrypted version was. 14 | 15 | ## Instructions 16 | Just run the pm_db.py file. If you would like to setup a new vault, delete the pm_db.mmf file. Then, reopen pm_db.py and it will automatically walk you through the setup steps. 17 | 18 | ## Demo 19 | You can run the demo by opening the pm_db.py file and using **thisisatest!** as the password. 20 | 21 | ## TODO 22 | - ~~**Password generator**~~: You can now generate truely random and secure passwords of a desired length. 23 | - ~~**Better search**~~: Find profile without knowing the website url exactly. Debating if the delete feature should have this function? 24 | - ~~**Data scrubbing**~~: Your activity won't be logged in terminal output. 25 | - ~~**Timeout after 90 seconds idle**~~ 26 | - ~~**Fix entering blank input causes timeout**~~ Now available **across platforms**. 27 | - ~~**Fix backspacing**~~: If you make a mistake, you have to go through the process again. Not terrible, but inconvienient. 28 | - ~~**Auto Copy & Paster Logins**~~: Function for user to export username/password to clipboard 29 | - **Turn into CLI tool?** 30 | - **Make it look sexy**: Taking a look at adding this: https://github.com/willmcgugan/rich 31 | - ~~**Keep command input lowercased**~~ 32 | - **Use Argon2 for hashing**: Uses Scrypt right now. 33 | - ~~**Added requirements.txt**~~ 34 | - ~~**Clear clipboard after pasting**~~ 35 | - **Create timeout function for password entry** 36 | 37 | ## Shoutouts 38 | - @aarana14 for doing some major cleanup and formatting of the main code. Much simpler to add future features and debug now! 39 | - @Kleysley for creating Change Master Password function. 40 | - @deep-bhatt 41 | - @maxdunbar 42 | - @gileadecastro 43 | 44 | ## Disclaimer 45 | This was built for educational purposes. This Password Manager is used at your own risk. This software is provided as is and I (including any contributors) do not take any responsibility for any damage or loss done with or by it. 46 | -------------------------------------------------------------------------------- /pm_db.py: -------------------------------------------------------------------------------- 1 | import json 2 | import base64 3 | import random 4 | from cryptography.hazmat.primitives.kdf.scrypt import Scrypt 5 | from cryptography.fernet import Fernet 6 | import getpass 7 | import os 8 | import threading 9 | import difflib 10 | import string 11 | import secrets 12 | import pyperclip 13 | import time 14 | from inputimeout import inputimeout, TimeoutOccurred 15 | import keyboard as kb 16 | import sys 17 | from god_key_hasher import * 18 | 19 | 20 | divider = "-----------------------------------------------------------------------------------------------------------------------\n" 21 | lockImg = """ 22 | 23 | ^jEQBQDj^ 24 | r#@@@@@@@@@#r 25 | ?@@@#x_`_v#@@@x 26 | g@@@! !@@@Q 27 | Q@@@_ _@@@B 28 | rgg@@@@QgggggQ@@@@ggr 29 | Y@@@@@@@@@@@@@@@@@@@Y 30 | Y@@@@@@@Qx^xQ@@@@@@@Y 31 | Y@@@@@@@^ ~@@@@@@@Y 32 | Y@@@@@@@@r r#@@@@@@@Y 33 | Y@@@@@@@@c,c@@@@@@@@Y 34 | Y@@@@@@@@@@@@@@@@@@@Y 35 | v###################v 36 | 37 | 38 | """ 39 | checkImg = """ 40 | 41 | `xx. 42 | 'k#@@@h` 43 | _m@@@@@@Q, 44 | "M@@@@@@$* 45 | `xk< =N@@@@@@9= 46 | T#@@@Qr ^g@@@@@@5, 47 | y@@@@@@Bv ?Q@@@@@@s- 48 | `V#@@@@@#B@@@@@@w' 49 | `}#@@@@@@@@#T` 50 | vB@@@@Bx 51 | )ER) 52 | 53 | """ 54 | vaultImg = """ 55 | !wdEEEEEEEEEEEEEEEEEEEEEEEEEEEEdw~ 56 | M@@ZzzzzzzzzzzzzzzzzzzzzzzzzzzzzZ@@6` 57 | \@@: !vvxvvvvvvvvvvvvvvvvvvvvvxv~ :@@L 58 | x@@` 0@@@@@@@@@@@@@@@@@@@@@@@@@@Q `@@c 59 | x@@` $@@@@@@@@@@@@@@@@@@@@@@@@@@Q `@@c 60 | x@@` $@@@@@@@@@@@@@@@@@@@@@@@@#Tr `@@c 61 | x@@` $@@@@#I)!,,~L6@@@@@@@@@@@m `@@c 62 | x@@` $@@@v`L$@###M!-6@@@@@@@@@3 `@@c 63 | x@@` $@@)`8@x` ,d@zT@@@@@@@@@@MT `@@c 64 | x@@` $@@ r@3 !@@@@@@@Q `@@c 65 | x@@` $@@r`Q@\` _Z@z}#@@@@@@@@0-` `@@c 66 | x@@` $@@@)`T8@B##Z~-d@@@@@@@@@m `@@c 67 | x@@` $@@@@Bz*:,,!xd@@@@@@@@@@@E` `@@c 68 | x@@` $@@@@@@@@@@@@@@@@@@@@@@@@@@Q `@@c 69 | x@@` $@@@@@@@@@@@@@@@@@@@@@@@@@@Q `@@c 70 | x@@` $@@@@@@@@@@@@@@@@@@@@@@@@@@Q `@@c 71 | \@@: !LLLLLLLLLLLLLLLLLLLLLLLLLL> :@@L 72 | `d@@MwwwwwwwwwwwwwwwwwwwwwwwwwwwwM@@E` 73 | ~z6Q@@@@@@$0$$$$0$$0$$0$@@@@@@B6z> 74 | ,EEEEEd ZEEEEE! 75 | """ 76 | 77 | # Global Variables 78 | timeoutGlobalCode = "*TIMEOUT*" 79 | 80 | def main(): 81 | # RUN PROGRAM 82 | # Check if vault exists 83 | try: 84 | file = open("pm_db.mmf", "r+") 85 | file.close() 86 | except: 87 | # If failed to open 88 | print(vaultImg) 89 | print("\nVAULT SETUP\n\nCould not find pm_db.mmf in local directory, continuing to vault setup.") 90 | print(vaultSetup()) 91 | 92 | 93 | # RUN LOGIN 94 | os.system("cls" if os.name == "nt" else "clear") 95 | print(lockImg) 96 | hashed_pass = False 97 | cSALT, cVERIFIER, dataBase = fileSetup() 98 | while not hashed_pass: 99 | entered_pass = getpass.getpass("Enter Master Key: ") 100 | hashed_pass = verify_password( 101 | entered_pass, cSALT, cVERIFIER 102 | ) # Require password to be entered 103 | if not hashed_pass: 104 | print("Incorrect master password. Try again.\n") 105 | if hashed_pass: 106 | del entered_pass 107 | main_pwd_manager(hashed_pass, dataBase) 108 | del hashed_pass 109 | del cSALT 110 | del cVERIFIER 111 | del dataBase 112 | 113 | 114 | def main_pwd_manager(hashed_pass, contents): 115 | os.system("cls" if os.name == "nt" else "clear") 116 | db = json.loads(decrypt_data(contents, hashed_pass).decode("utf-8")) 117 | timedOut = False 118 | while not timedOut: 119 | os.system("cls" if os.name == "nt" else "clear") 120 | print(checkImg) 121 | print(divider) 122 | user_cmd = print( 123 | "\n(a)dd profile | (f)ind profile data | (e)dit profile data | (r)ead all profiles | (d)elete profile data\n(g)enerate password | (c)hange master password | e(x)it\n" 124 | ) 125 | user_cmd = timeoutInput("What would you like to do? ") 126 | print("\n") 127 | 128 | # Ensure user input is lowercase 129 | if user_cmd != timeoutGlobalCode: 130 | user_cmd = user_cmd.lower() 131 | 132 | # Add Profile 133 | if user_cmd == "a": 134 | timedOut = addProfile(hashed_pass, db) 135 | 136 | # READ PROFILE 137 | if user_cmd == "f": 138 | timedOut = findProfileData(hashed_pass, db) 139 | 140 | # READ ALL PROFILES 141 | if user_cmd == "r": 142 | timedOut = readAllProfiles(hashed_pass, db) 143 | 144 | # EDIT PROFILE 145 | if user_cmd == "e": 146 | timedOut = editProfileData(hashed_pass, db) 147 | 148 | # DELETE PROFILE 149 | if user_cmd == "d": 150 | timedOut = deleteProfileData(hashed_pass, db) 151 | 152 | # GENERATE PASSWORD 153 | if user_cmd == "g": 154 | timedOut = pwdGenerate(hashed_pass, db) 155 | 156 | # CHANGE MASTER PASSWORD 157 | if user_cmd == "c": 158 | timedOut = changeMasterPassword(hashed_pass, db) 159 | 160 | # EXIT PROGRAM AND RETURN TO TERMINAL 161 | if user_cmd == "x": 162 | os.system("cls" if os.name == "nt" else "clear") 163 | timedOut = True 164 | 165 | # EXIT BECAUSE OF TIMEOUT 166 | if user_cmd == timeoutGlobalCode: 167 | timeoutCleanup() 168 | timedOut = True 169 | 170 | # CLEANUP SENSITIVE INFO ON TIMEOUT 171 | del hashed_pass 172 | del contents 173 | del db 174 | 175 | 176 | def changeMasterPassword(hashed_pass, db): 177 | # CHANGE MASTER PASSWORD 178 | displayHeader("CHANGE MASTER PASSWORD") 179 | password_provided = timeoutInput("What would you like your master password to be (type and submit (.c) to cancel)? ") 180 | if password_provided != ".c" and password_provided != "" and password_provided != " " and password_provided != timeoutGlobalCode: 181 | password = password_provided.encode() # Convert to type bytes 182 | salt = os.urandom(random.randint(16, 256)) 183 | kdf = Scrypt( 184 | salt=salt, 185 | length=32, 186 | n=2 ** 14, 187 | r=8, 188 | p=1, 189 | ) 190 | hashed_entered_pass = base64.urlsafe_b64encode(kdf.derive(password)) # Can only use kdf once 191 | try: 192 | i = -1 193 | domains = list(db.keys()) 194 | for e in db: 195 | i = i + 1 196 | 197 | # decrypt the username and password with the original master password 198 | username = str( 199 | decrypt_data( 200 | bytes(db[domains[i]]["username"], encoding="utf-8"), hashed_pass 201 | ).decode("utf-8") 202 | ) 203 | 204 | password = str( 205 | decrypt_data( 206 | bytes(db[domains[i]][ "password"], encoding="utf-8"), 207 | hashed_pass, 208 | ).decode("utf-8") 209 | ) 210 | 211 | # encrypt and save them with then new master password 212 | db[domains[i]] = { 213 | "username": str(encrypt_data(username, hashed_entered_pass).decode("utf-8")), 214 | "password": str(encrypt_data(password, hashed_entered_pass).decode("utf-8")), 215 | } 216 | 217 | del e 218 | del username 219 | del password 220 | 221 | del domains 222 | file = open("SALT.txt", "wb") 223 | file.write(salt) 224 | file.close() 225 | del salt 226 | 227 | file = open("VERIFIER.txt", "wb") 228 | file.write(encrypt_data("entered_master_correct", hashed_entered_pass)) 229 | file.close() 230 | 231 | # finally overwrite the database file with everything encrypted with the new password 232 | overwrite_db(encrypt_data(json.dumps(db), hashed_entered_pass).decode("utf-8")) 233 | del hashed_entered_pass 234 | del hashed_pass 235 | os.system("cls" if os.name == "nt" else "clear") 236 | print("Master password changed successfully! Log in again to access the password manager.") 237 | timeoutInput("\nPress enter to logout..") 238 | return True 239 | except: 240 | print("Could not change master password (Error code: 01)") 241 | userContinue = timeoutInput("\nPress enter to return to menu...") 242 | if userContinue != timeoutGlobalCode: 243 | return False 244 | else: 245 | return True 246 | else: 247 | if password_provided != timeoutGlobalCode: 248 | userContinue = timeoutInput("\nPress enter to return to menu...") 249 | if userContinue != timeoutGlobalCode: 250 | return False 251 | else: 252 | return True 253 | else: 254 | return True 255 | 256 | 257 | def addProfile(hashed_pass, db): 258 | # ADD PROFILE 259 | displayHeader("ADD A PROFILE") 260 | print("Type and submit (.c) to cancel.") 261 | add_domain = timeoutInput("Website domain name: ") 262 | if add_domain != ".c" and add_domain != timeoutGlobalCode: 263 | add_user = timeoutInput("Username: ") 264 | if add_user != ".c" and add_user != timeoutGlobalCode: 265 | add_password = timeoutInput("Password: ") 266 | if add_domain != ".c" and add_domain != timeoutGlobalCode and add_user != timeoutGlobalCode and add_password != timeoutGlobalCode: 267 | db[add_domain] = { 268 | "username": str(encrypt_data(add_user, hashed_pass).decode("utf-8")), 269 | "password": str(encrypt_data(add_password, hashed_pass).decode("utf-8")), 270 | } 271 | overwrite_db(encrypt_data(json.dumps(db), hashed_pass).decode("utf-8")) 272 | print("Created " + add_domain + " profile successfully!") 273 | if add_domain == ".c": 274 | print("Operation canceled.") 275 | return False 276 | if add_domain == timeoutGlobalCode or add_user == timeoutGlobalCode or add_password == timeoutGlobalCode: 277 | return True 278 | 279 | 280 | def findProfileData(hashed_pass, db): 281 | displayHeader("FIND A PROFILE") 282 | print("Type and submit (.c) to cancel.") 283 | read_domain = timeoutInput("What's the domain you're looking for? ") 284 | if read_domain != ".c" and read_domain != timeoutGlobalCode: 285 | try: 286 | domains = list(db.keys()) 287 | matches = difflib.get_close_matches(read_domain, domains) 288 | if matches: 289 | print("\nClosest match:\n") 290 | i = 1 291 | for d in matches: 292 | domain_info = db[d] 293 | username = str( 294 | decrypt_data( 295 | bytes(domain_info["username"], encoding="utf-8"), 296 | hashed_pass, 297 | ).decode("utf-8") 298 | ) 299 | print("PROFILE " + str(i) + ": " + d) 300 | del d 301 | print("Username: " + username + "\n") 302 | del domain_info 303 | del username 304 | i = i + 1 305 | userContinue = timeoutInput("\nSelect the password to be copied to your clipboard (ex: 1), or type (.c) to cancel: ") 306 | if userContinue.isdigit() == True: 307 | if int(userContinue) > 0: 308 | try: 309 | password = str( 310 | decrypt_data( 311 | bytes(db[str(matches[int(userContinue) - 1])]["password"], encoding="utf-8"), 312 | hashed_pass, 313 | ).decode("utf-8") 314 | ) 315 | print("\n" + to_clipboard(password)) 316 | del password 317 | except: 318 | print("\nUnable to find profile corresponding to " + str(userContinue) + ".") 319 | else: 320 | print("\nThere are no profiles corresponding to that number.") 321 | if userContinue.isdigit() == False: 322 | if userContinue != timeoutGlobalCode: 323 | return False 324 | else: 325 | return True 326 | else: 327 | print("Could not find a match. Try viewing all saved profiles.") 328 | except: 329 | print("Error finding profile.") 330 | userContinue = timeoutInput("\nPress enter to return to menu...") 331 | if userContinue != timeoutGlobalCode: 332 | return False 333 | else: 334 | return True 335 | if read_domain == ".c": 336 | print("Operation canceled.") 337 | print("\nReturning to Menu") 338 | return False 339 | if read_domain == timeoutGlobalCode: 340 | return True 341 | 342 | 343 | def editProfileData(hashed_pass, db): 344 | displayHeader("EDIT A PROFILE") 345 | edit_domain = timeoutInput("Website domain name (submit (.c) to cancel): ") 346 | if edit_domain != ".c" and edit_domain != timeoutGlobalCode: 347 | try: 348 | domain_info = db[edit_domain] 349 | curr_user = str( 350 | decrypt_data( 351 | bytes(domain_info["username"], encoding="utf-8"), hashed_pass 352 | ).decode("utf-8") 353 | ) 354 | curr_password = str( 355 | decrypt_data( 356 | bytes(domain_info["password"], encoding="utf-8"), hashed_pass 357 | ).decode("utf-8") 358 | ) 359 | 360 | edit_user = timeoutInput("New Username (press enter to keep the current: " + curr_user + "): ") 361 | if edit_user == ".c" or edit_user == " " or edit_user == "": 362 | edit_user = curr_user 363 | if edit_user == timeoutGlobalCode: 364 | return True 365 | 366 | edit_password = timeoutInput("New Password (press enter to keep the current: " + curr_password + "): ") 367 | if edit_password == ".c" or edit_password == " " or edit_user == "": 368 | edit_password = curr_password 369 | if edit_password == timeoutGlobalCode: 370 | return True 371 | 372 | db[edit_domain] = { 373 | "username": str(encrypt_data(edit_user, hashed_pass).decode("utf-8")), 374 | "password": str( 375 | encrypt_data(edit_password, hashed_pass).decode("utf-8") 376 | ), 377 | } 378 | overwrite_db(encrypt_data(json.dumps(db), hashed_pass).decode("utf-8")) 379 | print("Updated " + edit_domain + " profile successfully!") 380 | del edit_domain 381 | del curr_user 382 | del edit_user 383 | del curr_password 384 | del edit_password 385 | del db 386 | userContinue = timeoutInput("\nPress enter to return to menu...") 387 | if userContinue != timeoutGlobalCode: 388 | print("Returning to menu") 389 | return False 390 | else: 391 | return True 392 | except: 393 | print("This domain does not exist, changing to adding to new profile") 394 | userContinue = timeoutInput("\nPress enter to return to menu...") 395 | if userContinue != timeoutGlobalCode: 396 | print("Returning to menu") 397 | return False 398 | else: 399 | return True 400 | if edit_domain != timeoutGlobalCode: 401 | print("Returning to menu") 402 | return False 403 | else: 404 | return True 405 | 406 | 407 | def readAllProfiles(hashed_pass, db): 408 | displayHeader("READING ALL PROFILES") 409 | try: 410 | i = 0 411 | domains = list(db.keys()) 412 | for e in db: 413 | i = i + 1 414 | username = str( 415 | decrypt_data( 416 | bytes(db[e]["username"], encoding="utf-8"), hashed_pass 417 | ).decode("utf-8") 418 | ) 419 | print("PROFILE " + str(i) + ": " + e) 420 | print("Username: " + username) 421 | del e 422 | del username 423 | print(divider) 424 | if i == 0: 425 | print("No saved profiles") 426 | if i > 0: 427 | userContinue = timeoutInput("\nSelect the password to be copied to your clipboard (ex: 1), or type (.c) to cancel: ") 428 | if userContinue.isdigit() == True: 429 | if int(userContinue) > 0: 430 | try: 431 | password = str( 432 | decrypt_data( 433 | bytes(db[str(domains[int(userContinue) - 1])]["password"], encoding="utf-8"), 434 | hashed_pass, 435 | ).decode("utf-8") 436 | ) 437 | print("\n" + to_clipboard(password)) 438 | del password 439 | except: 440 | print("\nUnable to find profile corresponding to " + str(userContinue) + ".") 441 | else: 442 | print("\nThere are no profiles corresponding to that number.") 443 | if userContinue.isdigit() == False and userContinue != timeoutGlobalCode: 444 | return False 445 | if userContinue == timeoutGlobalCode: 446 | return True 447 | except: 448 | print("Could not load all profiles") 449 | userContinue = timeoutInput("\nPress enter to return to menu...") 450 | if userContinue != timeoutGlobalCode: 451 | print("Returning to menu") 452 | return False 453 | else: 454 | return True 455 | 456 | 457 | def deleteProfileData(hashed_pass, db): 458 | displayHeader("DELETE A PROFILE") 459 | del_domain = timeoutInput("Write the exact saved domain name (type (.c) to cancel): ") 460 | if del_domain != ".c" and del_domain != timeoutGlobalCode: 461 | try: 462 | del db[del_domain] 463 | overwrite_db(encrypt_data(json.dumps(db), hashed_pass).decode("utf-8")) 464 | print("Deleted " + del_domain + " profile successfully!") 465 | userContinue = timeoutInput("\nPress enter to return to menu...") 466 | if userContinue != timeoutGlobalCode: 467 | print("Returning to menu") 468 | return False 469 | else: 470 | return True 471 | except: 472 | print("Unable to find " + del_domain) 473 | userContinue = timeoutInput("\nPress enter to return to menu...") 474 | if userContinue != timeoutGlobalCode: 475 | print("Returning to menu") 476 | return False 477 | else: 478 | return True 479 | else: 480 | if del_domain != timeoutGlobalCode: 481 | print("Returning to menu") 482 | return False 483 | else: 484 | return True 485 | 486 | 487 | def pwdGenerate(hashed_pass, db): 488 | displayHeader("GENERATE RANDOM PASSWORD") 489 | pass_length = str(timeoutInput("Password length (type (.c) to cancel): ")) 490 | if pass_length != ".c" and pass_length != timeoutGlobalCode: 491 | try: 492 | if int(pass_length) < 6: 493 | pass_length = str(12) 494 | print("\nPasswords must be at least 6 characters long.") 495 | print(to_clipboard(str(generate_password(int(pass_length))))) 496 | userContinue = timeoutInput("\nPress enter to return to menu...") 497 | if userContinue != timeoutGlobalCode: 498 | print("Returning to menu") 499 | return False 500 | else: 501 | return True 502 | except: 503 | print("Unable to generate password.") 504 | userContinue = timeoutInput("\nPress enter to return to menu...") 505 | if userContinue != timeoutGlobalCode: 506 | print("Returning to menu") 507 | return False 508 | else: 509 | return True 510 | else: 511 | if pass_length != timeoutGlobalCode: 512 | print("Returning to menu") 513 | return False 514 | else: 515 | return True 516 | 517 | 518 | def fileSetup(): 519 | with open("SALT.txt", "rb") as readfile: 520 | content1 = readfile.read() 521 | readfile.close() 522 | cSALT = content1 523 | 524 | with open("VERIFIER.txt", "rb") as readfile: 525 | content2 = readfile.read() 526 | readfile.close() 527 | cVERIFIER = content2 528 | 529 | file_path = "pm_db.mmf" 530 | file = open(file_path, "rb") 531 | content3 = file.read() 532 | dataBase = content3 533 | 534 | return cSALT, cVERIFIER, dataBase 535 | 536 | 537 | def displayHeader(title): 538 | os.system("cls" if os.name == "nt" else "clear") 539 | print(checkImg) 540 | print(divider) 541 | print(str(title) + "\n") 542 | 543 | 544 | # Clear clipboard after 30 seconds 545 | def clear_clipboard_timer(): 546 | kb.wait('ctrl+v') 547 | time.sleep(0.1) # Without sleep, clipboard will automatically clear before user actually pastes content 548 | pyperclip.copy("") 549 | 550 | 551 | # Put string in clipboard 552 | def to_clipboard(input_to_copy): 553 | pyperclip.copy(str(input_to_copy)) 554 | del input_to_copy 555 | threading.Thread(target=clear_clipboard_timer).start() 556 | return "Password was saved to clipboard. It will be removed from your clipboard as soon as you paste it." 557 | 558 | 559 | # TIMEOUT 560 | def timeoutCleanup(): 561 | os.system("cls" if os.name == "nt" else "clear") 562 | print(lockImg) 563 | print( 564 | "\n\nYour session expired. For your security, the program has automatically exited. All submitted data is still saved." 565 | ) 566 | sys.exit 567 | 568 | 569 | def timeoutInput(caption): 570 | try: 571 | user_input = inputimeout(prompt=caption, timeout=90) 572 | except TimeoutOccurred: 573 | user_input = timeoutGlobalCode 574 | timeoutCleanup() 575 | return(user_input) 576 | 577 | 578 | # CRYPTOGRAPHY FUNCTIONS 579 | 580 | # Generate random password - user cannot request passwords that are less than 6 characters 581 | # use secrets instead of random (secrets is safer) 582 | def generate_password(length=12): 583 | if length < 6: 584 | length = 12 585 | uppercase_loc = secrets.choice(string.digits) # random location of lowercase 586 | symbol_loc = secrets.choice(string.digits) # random location of symbols 587 | lowercase_loc = secrets.choice(string.digits) # random location of uppercase 588 | password = "" 589 | pool = string.ascii_letters + string.punctuation # the selection of characters used 590 | for i in range(length): 591 | if i == uppercase_loc: # this is to ensure there is at least one uppercase 592 | password += secrets.choice(string.ascii_uppercase) 593 | elif i == lowercase_loc: # this is to ensure there is at least one uppercase 594 | password += secrets.choice(string.ascii_lowercase) 595 | elif i == symbol_loc: # this is to ensure there is at least one symbol 596 | password += secrets.choice(string.punctuation) 597 | else: # adds a random character from pool 598 | password += secrets.choice(pool) 599 | return password 600 | 601 | 602 | def encrypt_data(input, hashed_pass): 603 | message = input.encode() 604 | f = Fernet(hashed_pass) 605 | encrypted = f.encrypt(message) 606 | return encrypted 607 | 608 | 609 | def decrypt_data(input, hashed_pass): 610 | f = Fernet(hashed_pass) 611 | decrypted = f.decrypt(input) 612 | return decrypted 613 | 614 | 615 | def verify_password(password_provided, cSALT, cVERIFIER): 616 | verifier = cVERIFIER 617 | # Hash password for later comparison 618 | password = password_provided.encode() # Convert to type bytes 619 | salt = cSALT 620 | kdf = Scrypt( 621 | salt=salt, 622 | length=32, 623 | n=2**14, 624 | r=8, 625 | p=1, 626 | ) 627 | hashed_entered_pass = base64.urlsafe_b64encode( 628 | kdf.derive(password) 629 | ) # Can only use kdf once 630 | 631 | try: 632 | pass_verifier = decrypt_data(verifier, hashed_entered_pass) 633 | if pass_verifier == b"entered_master_correct": 634 | return hashed_entered_pass 635 | except: 636 | return False 637 | 638 | 639 | # PROFILE OPERATIONS 640 | def overwrite_db(new_contents): 641 | file = open("pm_db.mmf", "w+") 642 | file.write(new_contents) 643 | file.close() 644 | 645 | 646 | if __name__ == "__main__": 647 | main() 648 | --------------------------------------------------------------------------------