├── requirements.txt ├── screenshots ├── help.png ├── browse.png ├── reset.png ├── update_mpw.png ├── adding_account.png ├── get_by_domain.png ├── add_retrieve_pw.png ├── initializing_vault.png └── get_by_domain_options.png ├── .gitignore ├── utils ├── vault_utils.py ├── help.py ├── password_input.py ├── verify_vault.py ├── reset_vault.py ├── crypto_utils.py ├── generate_password.py ├── update_master.py ├── add_account.py ├── vault_init.py ├── get_account.py └── browse_vault.py ├── vaultec.py └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | cryptography 2 | InquirerPy 3 | rich 4 | pyfiglet -------------------------------------------------------------------------------- /screenshots/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/33Anubis/vaultec/HEAD/screenshots/help.png -------------------------------------------------------------------------------- /screenshots/browse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/33Anubis/vaultec/HEAD/screenshots/browse.png -------------------------------------------------------------------------------- /screenshots/reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/33Anubis/vaultec/HEAD/screenshots/reset.png -------------------------------------------------------------------------------- /screenshots/update_mpw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/33Anubis/vaultec/HEAD/screenshots/update_mpw.png -------------------------------------------------------------------------------- /screenshots/adding_account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/33Anubis/vaultec/HEAD/screenshots/adding_account.png -------------------------------------------------------------------------------- /screenshots/get_by_domain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/33Anubis/vaultec/HEAD/screenshots/get_by_domain.png -------------------------------------------------------------------------------- /screenshots/add_retrieve_pw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/33Anubis/vaultec/HEAD/screenshots/add_retrieve_pw.png -------------------------------------------------------------------------------- /screenshots/initializing_vault.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/33Anubis/vaultec/HEAD/screenshots/initializing_vault.png -------------------------------------------------------------------------------- /screenshots/get_by_domain_options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/33Anubis/vaultec/HEAD/screenshots/get_by_domain_options.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.pyc 4 | *.pyo 5 | *.pyd 6 | 7 | # Virtual environment 8 | venv/ 9 | 10 | # Vault file not to be tracked lol 11 | vault.json 12 | 13 | # OS cruft 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /utils/vault_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | VAULT_PATH = "vault.json" 5 | 6 | 7 | def vault_exists(): 8 | return os.path.exists(VAULT_PATH) 9 | 10 | 11 | def load_vault(): 12 | with open(VAULT_PATH, "r") as f: 13 | return json.load(f) 14 | 15 | 16 | def save_vault(data): 17 | with open(VAULT_PATH, "w") as f: 18 | json.dump(data, f, indent=2) 19 | -------------------------------------------------------------------------------- /utils/help.py: -------------------------------------------------------------------------------- 1 | from rich import print 2 | from pyfiglet import Figlet 3 | def help(): 4 | 5 | figlet = Figlet(font="slant") 6 | print(f"[cyan]{figlet.renderText('Vaultec')}[/cyan]") 7 | 8 | print("Usage:") 9 | print(" --init Initialize a new vault") 10 | print(" --add ") 11 | print(" --get Manage accounts for domain") 12 | print(" --browse View all domains & accounts") 13 | print(" --update-mpw Update master password") 14 | print(" --reset Reset the vault") 15 | print(" --help / -h View insturctions") 16 | -------------------------------------------------------------------------------- /utils/password_input.py: -------------------------------------------------------------------------------- 1 | # This module will check password length 2 | 3 | from getpass import getpass 4 | from InquirerPy import inquirer 5 | from utils.generate_password import generate_password 6 | from rich import print 7 | 8 | 9 | def prompt_password(): 10 | auto = inquirer.confirm( 11 | message="Generate a strong password automatically?", default=True 12 | ).execute() 13 | 14 | if auto: 15 | return generate_password() 16 | 17 | while True: 18 | pw = getpass("Enter password (8+ chars): ") 19 | if len(pw) < 8: 20 | print("[bold red]❌ Password too short.[/bold red]") 21 | continue 22 | return pw 23 | -------------------------------------------------------------------------------- /utils/verify_vault.py: -------------------------------------------------------------------------------- 1 | from getpass import getpass 2 | from utils.crypto_utils import hash_password 3 | from utils.vault_utils import vault_exists, load_vault 4 | from rich import print 5 | 6 | 7 | def verify_and_unlock_vault(): 8 | if not vault_exists(): 9 | print("Vault not found. Please initialize with '--init'.") 10 | return None, None, None # vault, password, salt 11 | 12 | vault = load_vault() 13 | salt = bytes.fromhex(vault["master"]["salt"]) 14 | stored_hash = bytes.fromhex(vault["master"]["hash"]) 15 | 16 | entered_pw = getpass("Enter your master password: ") 17 | hashed_attempt = hash_password(entered_pw, salt) 18 | 19 | if hashed_attempt != stored_hash: 20 | print("[bold red]❌ Access denied.[/bold red]") 21 | return None, None, None 22 | 23 | print("[bold green]✅ Access granted.[/bold green]") 24 | return vault, entered_pw, salt 25 | -------------------------------------------------------------------------------- /utils/reset_vault.py: -------------------------------------------------------------------------------- 1 | import os 2 | from utils.verify_vault import verify_and_unlock_vault 3 | from rich import print 4 | 5 | 6 | def reset_vault(): 7 | vault_path = "vault.json" 8 | 9 | if not os.path.exists(vault_path): 10 | print("No vault found to reset.") 11 | return 12 | 13 | authorized = verify_and_unlock_vault() 14 | if authorized: 15 | print("⚠️ WARNING: This will permanently delete your vault and all stored accounts.") 16 | confirm1 = input("Type 'RESET' (case-sensitive) to confirm: ") 17 | if confirm1 != "RESET": 18 | print("Reset aborted.") 19 | return 20 | 21 | confirm2 = input("Are you absolutely sure? Type 'YES' to proceed: ") 22 | if confirm2 != "YES": 23 | print("Reset aborted.") 24 | return 25 | 26 | os.remove(vault_path) 27 | print("💥 Vault wiped. It's gone. Forever.") 28 | else: 29 | print("[bold red]❌ Incorrect password.[/bold red]") 30 | print("[bold red]Access Denied.[/bold red]") 31 | -------------------------------------------------------------------------------- /utils/crypto_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import hashlib 3 | from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC 4 | from cryptography.hazmat.primitives import hashes 5 | from cryptography.hazmat.backends import default_backend 6 | import base64 7 | 8 | 9 | def generate_salt(): 10 | return os.urandom(16) 11 | 12 | 13 | def hash_password(password, salt, iterations=100_000): 14 | return hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iterations) 15 | 16 | 17 | def derive_fernet_key(password, salt): 18 | kdf = PBKDF2HMAC( 19 | algorithm=hashes.SHA256(), 20 | length=32, 21 | salt=salt, 22 | iterations=100_000, 23 | backend=default_backend(), 24 | ) 25 | return base64.urlsafe_b64encode(kdf.derive(password.encode())) 26 | 27 | 28 | def encrypt(fernet, plaintext): 29 | return fernet.encrypt(plaintext.encode()).decode() 30 | 31 | 32 | def decrypt(fernet, ciphertext): 33 | if isinstance(ciphertext, str): 34 | ciphertext = ciphertext.encode() 35 | return fernet.decrypt(ciphertext).decode() 36 | -------------------------------------------------------------------------------- /utils/generate_password.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | from InquirerPy import inquirer 4 | from rich import print 5 | 6 | 7 | def generate_password(): 8 | # Prompt for inputs to determine the totall length and how many digits and how many specials they want 9 | total_length = 0 10 | while total_length < 8: 11 | total_length = int( 12 | inquirer.number(message="Total password length (8+):", default=12).execute() 13 | ) 14 | if total_length < 8: 15 | print("Please select a length of 8 characters or more. 😐\n") 16 | 17 | num_digits = int( 18 | inquirer.number( 19 | message="How many digits?", min_allowed=0, max_allowed=total_length 20 | ).execute() 21 | ) 22 | 23 | num_special = int( 24 | inquirer.number( 25 | message="How many special characters?", 26 | min_allowed=0, 27 | max_allowed=total_length - num_digits, 28 | ).execute() 29 | ) 30 | 31 | num_letters = total_length - num_digits - num_special 32 | 33 | if int(num_letters) < 0: 34 | print("[bold red]❌ Invalid combination. Try again.[/bold red]") 35 | return generate_password() # Retry again 36 | 37 | # Character pools 38 | digits = random.choices(string.digits, k=num_digits) 39 | specials = random.choices("!@#$%^&*()-_=+[]{}", k=num_special) 40 | letters = random.choices(string.ascii_letters, k=num_letters) 41 | 42 | all_chars = digits + specials + letters 43 | random.shuffle(all_chars) 44 | 45 | password = "".join(all_chars) 46 | 47 | return password 48 | -------------------------------------------------------------------------------- /utils/update_master.py: -------------------------------------------------------------------------------- 1 | from getpass import getpass 2 | from utils.vault_utils import load_vault, save_vault 3 | from utils.crypto_utils import derive_fernet_key, hash_password, decrypt, encrypt 4 | from cryptography.fernet import Fernet 5 | from rich import print 6 | import os 7 | 8 | 9 | def update_master_password(): 10 | vault = load_vault() 11 | 12 | old_salt = bytes.fromhex(vault["master"]["salt"]) 13 | old_hash = bytes.fromhex(vault["master"]["hash"]) 14 | 15 | old_pw = getpass("Enter current master password: ") 16 | if hash_password(old_pw, old_salt) != old_hash: 17 | print("[bold red]❌ Incorrect password.[/bold red]") 18 | return 19 | 20 | # 1: Confirm new password 21 | while True: 22 | new_pw1 = getpass("Enter NEW master password (8+ chars): ") 23 | if len(new_pw1) < 8: 24 | print("Password too short.") 25 | continue 26 | new_pw2 = getpass("Confirm NEW master password: ") 27 | if new_pw1 != new_pw2: 28 | print("Passwords don't match. Try again.") 29 | continue 30 | break 31 | 32 | # 2: Re-encrypt entries 33 | old_key = Fernet(derive_fernet_key(old_pw, old_salt)) 34 | new_salt = os.urandom(16) 35 | new_key = Fernet(derive_fernet_key(new_pw1, new_salt)) 36 | 37 | for domain in vault["entries"]: 38 | for acc in vault["entries"][domain]: 39 | decrypted_pw = decrypt(old_key, acc["password"]) 40 | acc["password"] = encrypt(new_key, decrypted_pw) 41 | 42 | # 3: Update vault meta 43 | vault["master"]["salt"] = new_salt.hex() 44 | vault["master"]["hash"] = hash_password(new_pw1, new_salt).hex() 45 | 46 | save_vault(vault) 47 | print("[bold green]✅ Master password updated successfully.[/bold green]") 48 | -------------------------------------------------------------------------------- /utils/add_account.py: -------------------------------------------------------------------------------- 1 | from cryptography.fernet import Fernet 2 | from utils.crypto_utils import derive_fernet_key 3 | from getpass import getpass 4 | from InquirerPy import inquirer 5 | from utils.generate_password import generate_password 6 | from rich import print 7 | 8 | 9 | def add_account(vault, domain, username, master_password, salt): 10 | entries = vault.get("entries", {}) 11 | 12 | if domain not in entries: 13 | entries[domain] = [] 14 | 15 | domain_entries = entries[domain] 16 | 17 | # Check if this username already exists 18 | for account in domain_entries: 19 | if account["username"] == username: 20 | overwrite = input( 21 | f"Account '{username}' already exists under '{domain}'. Overwrite password? (y/n): " 22 | ) 23 | if overwrite.lower() != "y": 24 | print("Aborted.") 25 | return 26 | break 27 | 28 | # Ask if user wants to generate a password 29 | use_generator = inquirer.confirm( 30 | message="Generate a strong password automatically?", default=True 31 | ).execute() 32 | 33 | if use_generator: 34 | pw = generate_password() 35 | 36 | # Optional: Show user and let them copy it 37 | show_pw = inquirer.confirm( 38 | message="Show the generated password?", default=True 39 | ).execute() 40 | 41 | if show_pw: 42 | print(f"\n📋 Copy this password now: {pw}\n") 43 | else: 44 | pw = getpass("Enter the password to store: ") 45 | 46 | # Derive Fernet key and encrypt 47 | key = derive_fernet_key(master_password, salt) 48 | fernet = Fernet(key) 49 | encrypted_pw = fernet.encrypt(pw.encode()).decode() 50 | 51 | # Update or add new 52 | for account in domain_entries: 53 | if account["username"] == username: 54 | account["password"] = encrypted_pw 55 | break 56 | else: 57 | domain_entries.append({"username": username, "password": encrypted_pw}) 58 | 59 | entries[domain] = domain_entries 60 | vault["entries"] = entries 61 | 62 | print(f"[bold green]✅ Password for[/bold green] [cyan]{username}[/cyan] [bold green]under[/bold green] [magenta]{domain}[/magenta] [bold green]saved.[/bold green]") 63 | -------------------------------------------------------------------------------- /vaultec.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from utils.verify_vault import verify_and_unlock_vault 3 | from utils.vault_init import vault_init 4 | from utils.add_account import add_account 5 | from utils.vault_utils import save_vault 6 | from utils.get_account import get_account 7 | from utils.update_master import update_master_password 8 | from utils.reset_vault import reset_vault 9 | from utils.browse_vault import browse_vault 10 | from utils.help import help 11 | from rich import print 12 | from rich.panel import Panel 13 | 14 | 15 | def main(): 16 | banner = print(Panel("Welcome to [bold cyan]Vaultec[/bold cyan] 🔐", style="blue")) 17 | 18 | if len(sys.argv) > 1: 19 | cmd = sys.argv[1] 20 | if cmd == "--init": 21 | banner 22 | vault_init() 23 | elif cmd == "--verify": 24 | banner 25 | verify_and_unlock_vault() 26 | elif cmd == "--add": 27 | banner 28 | if len(sys.argv) < 4: 29 | print("Usage: add ") 30 | return 31 | 32 | domain = str(sys.argv[2]) 33 | username = str(sys.argv[3]) 34 | 35 | vault, master_pw, salt = verify_and_unlock_vault() 36 | if not vault: 37 | return 38 | 39 | add_account(vault, domain, username, master_pw, salt) 40 | save_vault(vault) 41 | elif cmd == "--get": 42 | banner 43 | if len(sys.argv) < 3: 44 | print("Usage: get ") 45 | return 46 | 47 | domain = sys.argv[2] 48 | get_account(domain) 49 | elif cmd == "--update-mpw": 50 | banner 51 | if len(sys.argv) != 2: 52 | print("Usage: --update-mpw") 53 | return 54 | update_master_password() 55 | elif cmd == "--reset": 56 | banner 57 | if len(sys.argv) != 2: 58 | print("Usage: --reset") 59 | return 60 | reset_vault() 61 | elif cmd == "--browse": 62 | banner 63 | if len(sys.argv) != 2: 64 | print("Usage: --browse") 65 | return 66 | browse_vault() 67 | elif cmd == "--help" or cmd == "-h": 68 | help() 69 | else: 70 | print(f"Unknown command: {cmd}") 71 | print("Use '--help' or '-h' to view instructions") 72 | else: 73 | help() 74 | print("\nVersion 1.0 | Created by Michael Zakhary") 75 | 76 | 77 | if __name__ == "__main__": 78 | main() 79 | -------------------------------------------------------------------------------- /utils/vault_init.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from getpass import getpass 3 | from utils.crypto_utils import generate_salt, hash_password 4 | from utils.vault_utils import vault_exists, save_vault 5 | from pyfiglet import Figlet 6 | from rich import print 7 | 8 | 9 | def vault_init(): 10 | figlet = Figlet(font="slant") 11 | print(f"[cyan]{figlet.renderText('Vaultec')}[/cyan]") 12 | 13 | if vault_exists(): 14 | print("Vault already exists.") 15 | return 16 | 17 | # Created this to not DRY 18 | def exit_logic(key): 19 | if str(key).lower() == "q": 20 | sys.exit(1) 21 | 22 | # Created this to no DRY 23 | def reenter_logic(key): 24 | if str(key).lower() == "p": 25 | print("Resetting master password.") 26 | return True 27 | return False 28 | 29 | # initializing the two pw attempts to two diff values so the while loop can be entered 30 | # I had a bug here where I set both to None so the while loop was being skipped 31 | pw1, pw2 = "", " " 32 | while pw1 != pw2: 33 | pw1 = getpass("Set master password (8 characters minimum): ") 34 | 35 | if len(pw1) < 8: 36 | print( 37 | "\nPassword must be at least 8 characters.\n" 38 | "Press 'q' + enter to exit. Please press any other key to try again..." 39 | ) 40 | key = input() 41 | exit_logic(key) 42 | continue 43 | 44 | # checking for password match 45 | while pw2 != pw1: 46 | pw2 = getpass("Confirm master password: ") 47 | if pw2 != pw1: 48 | print("") 49 | print("Passwords do not match.") 50 | print( 51 | "Options: \n" 52 | "I. Enter 'q' + enter to exit.\n" 53 | "II. Press 'Enter' to input the confirmation password again.\n" 54 | "III. Enter 'p' + enter to re-enter your password from the start." 55 | ) 56 | key = input() 57 | # if q is selected, quit 58 | exit_logic(key) 59 | # if p is selected, reattempt confirmation password only 60 | if reenter_logic(key): 61 | break 62 | 63 | # generate salt and hash 64 | salt = generate_salt() 65 | hashed_pw = hash_password(pw1, salt) 66 | 67 | # store in JSON + JSON structure 68 | vault = { 69 | "master": {"salt": salt.hex(), "hash": hashed_pw.hex()}, 70 | "entries": {}, 71 | } 72 | 73 | # Create JSON vault 74 | save_vault(vault) 75 | 76 | print("[bold green]Vault initialized successfully.[/bold green]") 77 | -------------------------------------------------------------------------------- /utils/get_account.py: -------------------------------------------------------------------------------- 1 | from utils.vault_utils import load_vault 2 | from utils.crypto_utils import derive_fernet_key, hash_password, decrypt 3 | from InquirerPy import inquirer 4 | from cryptography.fernet import Fernet 5 | from getpass import getpass 6 | from utils.vault_utils import save_vault 7 | from utils.password_input import prompt_password 8 | from rich import print 9 | 10 | 11 | def get_account(domain): 12 | vault = load_vault() 13 | 14 | salt = bytes.fromhex(vault["master"]["salt"]) 15 | stored_hash = bytes.fromhex(vault["master"]["hash"]) 16 | 17 | master_pw = getpass("Enter your master password: ") 18 | if hash_password(master_pw, salt) != stored_hash: 19 | print("[bold red]❌ Access denied.[/bold red]") 20 | return 21 | 22 | key = derive_fernet_key(master_pw, salt) 23 | fernet = Fernet(key) 24 | 25 | entries = vault.get("entries", {}) 26 | 27 | if domain not in entries or not entries[domain]: 28 | print(f"No accounts found under '{domain}'.") 29 | return 30 | 31 | choices = [account["username"] for account in entries[domain]] 32 | choices.append("Back") 33 | 34 | selected = inquirer.select( 35 | message=f"Select account under '{domain}':", choices=choices 36 | ).execute() 37 | 38 | if selected == "Back": 39 | print("🔙 Returning to main menu.") 40 | return 41 | 42 | # Find selected account 43 | for acc in entries[domain]: 44 | if acc["username"] == selected: 45 | action = inquirer.select( 46 | message=f"What would you like to do with '{selected}'?", 47 | choices=["View password", "Update password", "Delete account", "Back"], 48 | ).execute() 49 | 50 | if action == "View password": 51 | decrypted_pw = decrypt(fernet, acc["password"]) 52 | print(f"\n🔐 Password for {selected}: {decrypted_pw}\n") 53 | 54 | elif action == "Update password": 55 | # new_pw = getpass("Enter new password: ") 56 | new_pw = prompt_password() 57 | encrypted_pw = fernet.encrypt(new_pw.encode()).decode() 58 | acc["password"] = encrypted_pw 59 | save_vault(vault) 60 | show_pw = inquirer.confirm( 61 | message="Show the generated password?", default=True 62 | ).execute() 63 | 64 | if show_pw: 65 | print(f"\n📋 Copy this password now: {new_pw}\n") 66 | print("[bold green]✅ Password updated.[/bold green]") 67 | 68 | elif action == "Delete account": 69 | confirm = inquirer.confirm( 70 | message=f"Are you sure you want to delete '{selected}'?", 71 | default=False, 72 | ).execute() 73 | if confirm: 74 | entries[domain].remove(acc) 75 | save_vault(vault) 76 | print("🗑️ Account deleted.") 77 | else: 78 | print("Deletion canceled.") 79 | else: 80 | print("Returning.") 81 | 82 | return 83 | 84 | print("Account not found. This shouldn't happen.") 85 | -------------------------------------------------------------------------------- /utils/browse_vault.py: -------------------------------------------------------------------------------- 1 | from utils.vault_utils import load_vault, save_vault 2 | from utils.crypto_utils import derive_fernet_key, decrypt, encrypt, hash_password 3 | from InquirerPy import inquirer 4 | from cryptography.fernet import Fernet 5 | from getpass import getpass 6 | from utils.password_input import prompt_password 7 | from rich import print 8 | 9 | 10 | def browse_vault(): 11 | vault = load_vault() 12 | 13 | # Get master password 14 | salt = bytes.fromhex(vault["master"]["salt"]) 15 | stored_hash = bytes.fromhex(vault["master"]["hash"]) 16 | 17 | master_pw = getpass("Enter your master password: ") 18 | if hash_password(master_pw, salt) != stored_hash: 19 | print("[bold red]❌ Access denied.[/bold red]") 20 | return 21 | 22 | fernet = Fernet(derive_fernet_key(master_pw, salt)) 23 | 24 | while True: 25 | domains = list(vault["entries"].keys()) 26 | if not domains: 27 | print("No domains found in your vault.") 28 | return 29 | 30 | selected_domain = inquirer.select( 31 | message="Select a domain (or press Esc to exit):", 32 | choices=domains + ["[Exit]"], 33 | ).execute() 34 | 35 | if selected_domain == "[Exit]": 36 | break 37 | 38 | accounts = vault["entries"].get(selected_domain, []) 39 | if not accounts: 40 | print(f"No accounts found under {selected_domain}.") 41 | continue 42 | 43 | while True: 44 | account_choices = [acc["username"] for acc in accounts] + ["[Back]"] 45 | selected_account = inquirer.select( 46 | message=f"Accounts under '{selected_domain}':", choices=account_choices 47 | ).execute() 48 | 49 | if selected_account == "[Back]": 50 | break 51 | 52 | account = next( 53 | acc for acc in accounts if acc["username"] == selected_account 54 | ) 55 | 56 | action = inquirer.select( 57 | message=f"What would you like to do with '{selected_account}'?", 58 | choices=[ 59 | "View password", 60 | "Update password", 61 | "Delete account", 62 | "[Back]", 63 | ], 64 | ).execute() 65 | 66 | if action == "View password": 67 | pw = decrypt(fernet, account["password"]) 68 | print(f"🔐 Password for {selected_account}: {pw}\n") 69 | 70 | elif action == "Update password": 71 | new_pw = prompt_password() 72 | account["password"] = encrypt(fernet, new_pw) 73 | save_vault(vault) 74 | show_pw = inquirer.confirm( 75 | message="Show the generated password?", default=True 76 | ).execute() 77 | 78 | if show_pw: 79 | print(f"\n📋 Copy this password now: {new_pw}\n") 80 | 81 | print("[bold green]✅ Password updated.[/bold green]") 82 | print("\nContinue browsing below:") 83 | 84 | elif action == "Delete account": 85 | confirm = input( 86 | f"Are you sure you want to delete '{selected_account}'? (y/n): " 87 | ) 88 | if confirm.lower() == "y": 89 | accounts.remove(account) 90 | save_vault(vault) 91 | print("[bold green]✅ Account deleted.[/bold green]") 92 | break # Go back to account list 93 | 94 | elif action == "[Back]": 95 | continue 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔐 Vaultec 2 | 3 | **Vaultec** is a lightweight, offline command-line password manager built with Python. 4 | 5 | It uses strong encryption to safely store multiple account credentials per domain, all protected by a master password you control. 6 | 7 | No cloud. No telemetry. Just secure local storage, with a clean CLI UI. 8 | 9 | # Features (v1.0) 10 | 11 | - Master password with salted hashing 12 | - Update your master password (automatically re-encrypts the vault) 13 | - AES-based vault encryption (via Fernet) 14 | - Store multiple accounts per domain 15 | - Interactive CLI with arrow-key navigation (InquirerPy) 16 | - Random password generator with custom rules: 17 | - Total length 18 | - Number of digits 19 | - Number of special characters 20 | - Update or delete credentials for any saved account 21 | - Browse entire vault or view accounts by domain 22 | - Styled output with Rich + ASCII banner via PyFiglet 23 | 24 | > ⚠️ Still in development – use at your own risk. Not intended for production or sensitive info (yet). 25 | 26 | # Quick Setup 27 | 28 | ```bash 29 | python3 -m venv venv 30 | source venv/bin/activate 31 | pip install -r requirements.txt 32 | python vaultec.py --init 33 | ``` 34 | 35 | # Commands 36 | 37 | ```python 38 | python3 vaultec.py --init # Initialize a new vault 39 | python3 vaultec.py --add # Add a new account 40 | python3 vaultec.py --get # View/update/delete an account under a domain 41 | python3 vaultec.py --browse # Browse all domains + accounts 42 | python3 vaultec.py --update-mpw # Update your master password 43 | python3 vaultec.py --reset # Wipe vault and start fresh 44 | ``` 45 | 46 | # What I Learned 47 | 48 | ## Encryption 49 | 50 | Before this project, "encryption" was a black box to me. Now, I understand it as a two-step process: 51 | 52 | ### Password Hashing 53 | 54 | - The master password (MPW) is hashed using a salted hash (PBKDF2-HMAC-SHA256) so that even if someone gets the vault file, they can't brute-force the actual password easily. 55 | 56 | - The salt (adding random characters) ensures that two users with the same password won’t generate the same hash. 57 | 58 | ### Fernet Key Derivation 59 | 60 | - From the master password + salt, I derive a Fernet key that encrypts/decrypts all account passwords. 61 | 62 | - If you change your master password, I decrypt every saved password using the old key, then re-encrypt them with a new key. Since, you know, the Fernet key is derived from the MPW. 63 | 64 | - That was tricky to wrap my head around, but it gave me a much deeper appreciation for what secure apps actually do under the hood. 65 | 66 | This was my first real dive into crypto, and I now feel comfortable building basic secure tools from scratch. 67 | 68 | ### Hashing (One-Way) 69 | 70 | Hashing is irreversible, you cannot get the original input back from a hash. 71 | 72 | - It’s typically used for verification, not storage. 73 | 74 | - Example use: storing password hashes in a database. 75 | - When a user logs in, their password is hashed using the same algorithm and salt, and if it matches the stored hash, they're authenticated. 76 | 77 | - A cryptographic hash function like SHA-256 or bcrypt always produces the same output for the same input, but with salting and key stretching (like PBKDF2), you make brute-force attacks much harder. 78 | 79 | ### Encryption (Two-Way) 80 | 81 | Encryption is reversible, if you have the key, you can decrypt the ciphertext and recover the original data. 82 | 83 | - It's used for secure storage and transport of data you eventually want to read again. 84 | 85 | - In Vaultec, you encrypt each account's password using a symmetric key (Fernet key) derived from the master password. 86 | 87 | - If someone gets the encrypted data without the key, it’s unreadable. But if they get the key? Game over, which is why you store only a hashed version of the master password, not the key itself. 88 | 89 | ## Modularity 90 | 91 | This was a crash course in why code structure matters. 92 | 93 | I noticed that logic for things like password validation, encryption, or user prompts ended up copy-pasted in multiple places. With more time, I would refactor those into shared utilities, for example: 94 | 95 | - A validate_password() function 96 | 97 | - A generate_password() wrapper 98 | 99 | Still, this taught me how to identify repeated logic and where abstraction starts to matter to organize code and make reusing it faster and easier and more consistent. 100 | 101 | ## Future Improvements 102 | 103 | 1. Refactor repetitive logic: like input validation and encryption steps : into shared utility functions. 104 | 105 | 2. Polish CLI styling: consistent use of rich and color choices that are readable but not overwhelming. 106 | 107 | 3. Stateful CLI session: avoid asking the user for the master password repeatedly by storing an in-memory key for the duration of a session. 108 | 109 | 4. Fuzzy search: type part of a domain name or username and instantly filter results. 110 | 111 | 5. Frontend GUI: after learning React or Electron, build a companion desktop app. 112 | 113 | 6. Import/export vault: backup vaults or sync across machines. 114 | 115 | 7. Unit tests and CI: to ensure security and regression protection. 116 | 117 | 8. Browser extension: future plan to autofill passwords directly in the browser. 118 | 119 | # Special thanks to Boot.dev for organizing the hackathon and giving me this chance to participate. 120 | 121 | # Screenshots 122 | 123 | #### Help 124 | ![Help command](./screenshots/help.png) 125 | #### Reset 126 | ![Reset command](./screenshots/reset.png) 127 | #### Intializing a vault 128 | ![Initializing Vault](./screenshots/initializing_vault.png) 129 | #### Adding an account 130 | ![Adding an account](./screenshots/adding_account.png) 131 | #### Adding + Retrieving a pw 132 | ![Adding and getting auto-generated password](./screenshots/add_retrieve_pw.png) 133 | #### Get by domain name 134 | ![Get by domain name](./screenshots/get_by_domain.png) 135 | #### Get by domain name - options 136 | ![Get by domain name - options](./screenshots/get_by_domain_options.png) 137 | #### Browsing 138 | ![Browse command](./screenshots/browse.png) 139 | #### Updating Master Password 140 | ![Update Master Password](./screenshots/update_mpw.png) 141 | --------------------------------------------------------------------------------