diff --git a/data/projects.json b/data/projects.json index a0ac29e..bf80dec 100644 --- a/data/projects.json +++ b/data/projects.json @@ -275,9 +275,9 @@ "starter_code": "starter_code/survey_form/index.html" }, { - "id":10, + "id": 10, "title": "API ETL Pipeline", - "skills": ["Python","pandas","requests"], + "skills": ["Python", "pandas", "requests"], "level": "Intermediate", "interest": "Data", "time": "Medium", @@ -289,17 +289,17 @@ "Generate summary statistics", "Export the processed CSV for any other Analytics projects" ], - "tech_stack": ["Python", "pandas","requests","JSON"], + "tech_stack": ["Python", "pandas", "requests", "JSON"], "roadmap": [ - "Step 1: Install required modules via pip", - "Step 2: Find a public API key for this project", - "Step 3: Fetch the data from the API using requests", - "Step 4: Validate the response you just fetched From the API", - "Step 5: Normalize the nested JSON data by flattening it", - "Step 6: Use the fetched data to build a pandas dataframe", - "Step 7: Handle missing values or duplicate values", - "Step 8: Export the cleaned dataset to CSV format", - "Step 9: Generate a summary for the newly created CSV dataset", + "Step 1: Install required modules via pip", + "Step 2: Find a public API key for this project", + "Step 3: Fetch the data from the API using requests", + "Step 4: Validate the response you just fetched From the API", + "Step 5: Normalize the nested JSON data by flattening it", + "Step 6: Use the fetched data to build a pandas dataframe", + "Step 7: Handle missing values or duplicate values", + "Step 8: Export the cleaned dataset to CSV format", + "Step 9: Generate a summary for the newly created CSV dataset", "Step 10: Test the file with at least two different public APIs" ], "resources": [ @@ -311,7 +311,7 @@ ], "starter_code": "starter_code/api_data_pipeline.py" }, - { + { "id": 11, "title": "AI Resume Analyzer", "skills": [ @@ -360,11 +360,42 @@ "Flask quickstart: https://flask.palletsprojects.com/quickstart" ], "starter_code": "starter_code/ai_resume_analyzer.py" - } + }, + { + "id": 12, + "title": "Secure Crypto Vault", + "skills": ["Python"], + "level": "Intermediate", + "interest": "Cybersecurity", + "time": "Medium", + "description": "A professional-grade CLI utility for storing sensitive credentials using industry-standard AES-256-GCM encryption. Teaches cryptography basics and secure key derivation.", + "features": [ + "Encrypt and store passwords securely", + "PBKDF2 key derivation for master passwords", + "Authenticated encryption (AES-GCM)", + "JSON-based local data persistence" + ], + "tech_stack": ["Python", "Cryptography library", "JSON", "base64"], + "roadmap": [ + "Step 1: Set up a virtual environment and install cryptography", + "Step 2: Implement PBKDF2 for secure master key generation", + "Step 3: Build the AES-GCM encryption/decryption logic", + "Step 4: Create functions to save/load the encrypted vault file", + "Step 5: Build a secure CLI menu for adding/viewing entries", + "Step 6: Implement salt and nonce handling for high security", + "Step 7: Test with multiple service entries and verify decryption" + ], + "resources": [ + "Python Cryptography docs: https://cryptography.io/en/latest/", + "OWASP Key Derivation: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html", + "Real Python Security: https://realpython.com/python-api-key-setup-and-usage/" + ], + "starter_code": "starter_code/secure_vault/valt.py" + } , { - "id": 8, + "id": 13, "title": "Number Guessing Game", "skills": ["Python"], "level": "Beginner", @@ -395,7 +426,7 @@ "starter_code": "starter_code/number_guessing.py" }, { - "id": 9, + "id": 14, "title": "Simple Email Automation", "skills": ["Python"], "level": "Beginner", @@ -426,7 +457,7 @@ "starter_code": "starter_code/email_automation.py" }, { - "id": 10, + "id": 15, "title": "Quiz App", "skills": ["HTML", "CSS", "JavaScript"], "level": "Beginner", diff --git a/starter_code/secure-vault/README.md b/starter_code/secure-vault/README.md new file mode 100644 index 0000000..6ddfe15 --- /dev/null +++ b/starter_code/secure-vault/README.md @@ -0,0 +1,39 @@ +# Secure Crypto Vault (Python) + +A professional-grade CLI utility demonstrating industry-standard security implementations for credential management. + +## Features +- **Authenticated Encryption:** Uses AES-256-GCM (Galois/Counter Mode) for both confidentiality and integrity. +- **Secure Key Derivation:** Implements PBKDF2-HMAC-SHA256 with 600,000 iterations (OWASP recommendation). +- **Master Password Protection:** Uses the `getpass` module to mask password entry. +- **Persistent Storage:** Encrypted credentials are saved to a local `vault.json` file. + +## Setup Instructions +1. Navigate to the project folder: + ```bash + cd starter_code/secure-vault + +2. **Install the required security library:** + ```bash + pip install -r requirements.txt + +3. **Run the application:** + ```bash + python vault.py + +## Security Implementation Details +This project prioritizes high-entropy security and follows modern cryptographic standards: + +- **Key Derivation (PBKDF2):** + - **Algorithm:** HMAC-SHA256 + - **Iterations:** 600,000 (Aligned with OWASP 2024 password storage recommendations). + - **Salt:** 16-byte cryptographically secure random salt generated via `os.urandom()`. + +- **Authenticated Encryption (AES-GCM):** + - **Mode:** AES-256-GCM (Galois/Counter Mode). + - **Integrity:** Unlike standard AES-CBC, GCM provides built-in authentication, ensuring that the ciphertext has not been tampered with. + - **Nonce:** A unique 12-byte initialization vector (IV) is generated for every individual entry. + +- **Storage:** + - Sensitive data is never stored in plain text. + - The `vault.json` file contains only the Salt, Nonce, and Ciphertext in hexadecimal format. \ No newline at end of file diff --git a/starter_code/secure-vault/requirements.txt b/starter_code/secure-vault/requirements.txt new file mode 100644 index 0000000..6af6ab0 Binary files /dev/null and b/starter_code/secure-vault/requirements.txt differ diff --git a/starter_code/secure-vault/vault.py b/starter_code/secure-vault/vault.py new file mode 100644 index 0000000..23f2c6d --- /dev/null +++ b/starter_code/secure-vault/vault.py @@ -0,0 +1,264 @@ +import json +import os +import secrets +import sys +import getpass # This module allows us to hide the password as the user types it +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.backends import default_backend + +# This is the name of the file where all our encrypted data will live +VAULT_FILE = "vault.json" + +def get_input(prompt): + """ + A helper function to get text from the user. + If the user types 'q', the program exits immediately. + """ + user_input = input(prompt).strip() + if user_input.lower() == 'q': + print("\nExiting Secure Vault. Stay safe!") + sys.exit() + return user_input + +def derive_key(password: str, salt: bytes) -> bytes: + """ + This is a Key Derivation Function (KDF). + It takes your easy-to-remember password and turns it into a + mathematically strong 32-byte key for the encryption algorithm. + """ + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=salt, + iterations=600000, # Running this 600k times makes it very slow for hackers to guess + backend=default_backend() + ) + return kdf.derive(password.encode()) + +def save_vault(data): + """Takes our Python dictionary and writes it into the JSON file.""" + with open(VAULT_FILE, 'w') as f: + json.dump(data, f, indent=4) + +def load_vault(): + """Tries to open the vault file. Returns None if the file is missing or broken.""" + if os.path.exists(VAULT_FILE): + try: + with open(VAULT_FILE, 'r') as f: + return json.load(f) + except (json.JSONDecodeError, IOError): + print("[ERROR] Vault file corrupted or unreadable.") + return None + return None + +def main(): + print("==============================") + print("--- SECURE CRYPTO VAULT ---") + print("(Type 'q' at any time to quit)") + print("==============================") + + # Step 1: Try to load existing vault data + vault = load_vault() + + if not vault: + # If no vault exists, we start the setup process + print("No vault detected. Let's set it up.") + master_pw = "" + while not master_pw.strip(): + # getpass.getpass hides the characters while typing for safety + master_pw = getpass.getpass("Create a strong Master Password: ").strip() + if master_pw.lower() == 'q': sys.exit() + if not master_pw.strip(): + print("[ERROR] Master Password cannot be empty!") + + # Salt is random data mixed with the password to prevent pre-computed attacks + salt = secrets.token_bytes(16) + temp_key = derive_key(master_pw, salt) + temp_aes = AESGCM(temp_key) + + # We create a 'test lock'. Later, if we can decrypt this, we know the password is correct. + temp_nonce = secrets.token_bytes(12) + test_lock = temp_aes.encrypt(temp_nonce, b"SUCCESS", None) + + # Prepare the dictionary that will be saved to vault.json + vault = { + "salt": salt.hex(), + "test_nonce": temp_nonce.hex(), + "test_lock": test_lock.hex(), + "entries": {} + } + save_vault(vault) + print("[SUCCESS] Vault created successfully!") + key = temp_key + else: + # If a vault exists, the user must log in + master_pw = getpass.getpass("Enter Master Password: ").strip() + if master_pw.lower() == 'q': sys.exit() + + # Load the saved salt and test data from the JSON + salt = bytes.fromhex(vault["salt"]) + test_nonce = bytes.fromhex(vault["test_nonce"]) + test_lock = bytes.fromhex(vault["test_lock"]) + + # Re-create the key from the entered password + key = derive_key(master_pw, salt) + aesgcm_test = AESGCM(key) + + try: + # Try to unlock the test message. If this fails, an exception is triggered. + aesgcm_test.decrypt(test_nonce, test_lock, None) + print("[ACCESS GRANTED]") + except Exception: + print("\n[ALERT] INCORRECT MASTER PASSWORD!") + print("TERMINATING SESSION FOR SECURITY.") + return + + # Once logged in, we initialize our encryption engine (AES-GCM) + aesgcm = AESGCM(key) + + while True: + print("\n--- Main Menu ---") + print("[1] List Services") + print("[2] Exit") + choice1 = get_input("Select an option: ") + + if not choice1: + print("[WARN] Choice cannot be empty!") + continue + + if choice1 == "1": + while True: + # Display the names of all services currently stored + print("\n--- Registered Services ---") + if not vault["entries"]: + print("(No services saved yet)") + else: + # sorted() ensures 'Apple' comes before 'Zillow' + for s in sorted(vault["entries"].keys()): + print(f" - {s}") + + print("\n--- Service Options ---") + print("[1] Add Password") + print("[2] Get Password") + print("[3] Display All Secrets") + print("[4] Delete Entry") + print("[5] Audit Metadata (View Raw JSON Storage)") + print("[6] Back") + choice2 = get_input("Select: ") + + if not choice2: + print("[WARN] Choice cannot be empty!") + continue + + if choice2 == "1": + # Option to encrypt and save a new set of credentials + service = "" + while not service.strip(): + service = get_input("Service Name (e.g., Google, GitHub...): ").lower() + if not service: print("[ERROR] Service name cannot be empty!") + + username = "" + while not username.strip(): + username = get_input("Username: ") + if not username: print("[ERROR] Username cannot be empty!") + + password = "" + while not password.strip(): + password = get_input("Password: ") + if not password: print("[ERROR] Password cannot be empty!") + + # A 'nonce' is a 'Number used ONCE'. It makes the encryption unique. + nonce = secrets.token_bytes(12) + combined_data = f"{username}|{password}".encode() + ciphertext = aesgcm.encrypt(nonce, combined_data, None) + + # Save both the encrypted data and the nonce needed to unlock it + vault["entries"][service] = {"nonce": nonce.hex(), "data": ciphertext.hex()} + save_vault(vault) + print(f"[SAVED] {service} encrypted and stored.") + + elif choice2 == "2": + # Option to find and decrypt a single password + service = get_input("Enter service name (e.g., Google, GitHub...): ").lower() + if service in vault["entries"]: + entry = vault["entries"][service] + nonce = bytes.fromhex(entry["nonce"]) + ciphertext = bytes.fromhex(entry["data"]) + + try: + # Decrypt the binary data back into a readable string + decrypted = aesgcm.decrypt(nonce, ciphertext, None).decode() + user, pw = decrypted.split("|") + print(f"\n[FOUND]\nUsername: {user}\nPassword: {pw}") + except: + print("[ERROR] Decryption failure.") + else: + print("[WARN] Service not found.") + + elif choice2 == "3": + # Decrypt every single entry in the vault and print it out + if not vault["entries"]: + print("[WARN] No secrets to display.") + else: + print("\n" + "="*25) + print("FULL VAULT DECRYPTION") + print("="*25) + for service, data in vault["entries"].items(): + nonce = bytes.fromhex(data["nonce"]) + ciphertext = bytes.fromhex(data["data"]) + try: + decrypted = aesgcm.decrypt(nonce, ciphertext, None).decode() + user, pw = decrypted.split("|") + print(f"SERVICE: {service.upper()}") + print(f" User: {user}") + print(f" Pass: {pw}") + print("-" * 20) + except: + print(f"[ERROR] Could not decrypt {service}") + print("="*25) + + elif choice2 == "4": + # Permanently remove an entry from the vault dictionary + service = get_input("Service to DELETE (e.g., Google, GitHub...): ").lower() + if service in vault["entries"]: + confirm = get_input(f"[CONFIRM] Permanently delete {service}? (y/n): ") + if confirm.lower() == 'y': + del vault["entries"][service] + save_vault(vault) + print(f"[DELETED] {service} removed.") + else: + print("Deletion cancelled.") + else: + print("[WARN] Service not found.") + + elif choice2 == "5": + # Shows the 'raw' encrypted text (what it looks like in the JSON file) + service = get_input("Audit service (e.g., Google, GitHub...): ").lower() + if service in vault["entries"]: + entry = vault["entries"][service] + print(f"\n--- RAW STORAGE AUDIT: {service.upper()} ---") + print(f"Ciphertext: {entry['data']}") + print(f"Nonce (IV): {entry['nonce']}") + print("Algorithm: AES-256-GCM") + print("-" * 40) + else: + print("[WARN] Service not found.") + + elif choice2 == "6": + # Exit the inner loop to return to the simple main menu + break + else: + print("[WARN] Invalid selection.") + + elif choice1 == "2": + # Close the program + print("Goodbye!") + break + else: + print("[WARN] Invalid selection.") + +if __name__ == "__main__": + # This checks if the script is being run directly rather than imported + main() \ No newline at end of file