init commit of KeyKoala

This commit is contained in:
Blake Ridgway 2025-02-21 21:46:12 -06:00
parent 751d67d75f
commit ef79fce1d6
13 changed files with 420 additions and 0 deletions

135
README.md Normal file
View file

@ -0,0 +1,135 @@
# 🐨 KeyKoala
<div align="center">
![KeyKoala Logo](assets/images/keykoala_logo.png)
A secure, lightweight, and user-friendly password manager built with Python.
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Python 3.7+](https://img.shields.io/badge/python-3.7+-blue.svg)](https://www.python.org/downloads/)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
</div>
## 🚀 Features
- 🔐 Master password protection
- 🔒 Strong encryption using Fernet
- 📋 Quick copy passwords to clipboard
- 💾 Secure local storage
- 🖥️ Clean, intuitive GUI
- 🏃 Lightweight and fast
- 📱 Cross-platform support
## 🛠️ Installation
1. Clone the repository
```bash
git clone https://github.com/yourusername/keykoala.git
cd keykoala
```
2. Create and activate virtual environment
```bash
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
3. Install dependencies
```bash
pip install -r requirements.txt
```
4. Install tkinter (if not already installed)
**Ubuntu/Debian:**
```bash
sudo apt-get install python3-tk
```
**Fedora:**
```bash
sudo dnf install python3-tkinter
```
**macOS:**
```bash
brew install python-tk
```
## 🏃‍♂️ Running KeyKoala
From the project root directory:
```bash
PYTHONPATH=$PYTHONPATH:$(pwd) python src/main.py
```
## 🔒 Security Features
- Strong encryption using Fernet (symmetric encryption)
- Master password never stored, only used for key derivation
- Passwords only decrypted when copying to clipboard
- All data stored locally on your machine
## 🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
## 📝 Development Tasks
- [ ] Password generation functionality
- [ ] Password strength checker
- [ ] Import/export functionality
- [ ] Dark mode support
- [ ] Password categories/folders
- [ ] Auto-logout feature
- [ ] Two-factor authentication
- [ ] Browser extension
## 🐛 Known Issues
- None reported yet
## 📦 Dependencies
- Python 3.7+
- tkinter
- cryptography
- pyperclip
## ⚠️ Disclaimer
KeyKoala is a demonstration project and should not be used as your primary password manager without thorough security auditing. For sensitive information, please use established password managers like Bitwarden, LastPass, or 1Password.
## 📄 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## 🙏 Acknowledgments
- Inspired by modern password managers
- Built with Python's cryptography library
- Uses tkinter for the GUI
## 📧 Contact
- Project Maintainer: [Blake Ridgway](mailto:blake@blakeridgway.com)
- Project Link: [https://github.com/blakeridgway/keykoala](https://github.com/blakeridgway/keykoala)
## 🌟 Support
If you find this project helpful, please give it a star! ⭐
---
<div align="center">
Made with ❤️ by Blake Ridgway
</div>
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

2
requirements.txt Normal file
View file

@ -0,0 +1,2 @@
cryptography>=3.4.7
pyperclip>=1.8.2

0
src/__init__.py Normal file
View file

0
src/crypto/__init__.py Normal file
View file

28
src/crypto/encryption.py Normal file
View file

@ -0,0 +1,28 @@
# src/crypto/encryption.py
import base64
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
class EncryptionManager:
def __init__(self, master_password, salt):
self.fernet = self._create_fernet(master_password, salt)
def _create_fernet(self, master_password, salt):
"""Create a Fernet instance using the master password and salt."""
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=480000,
)
key = base64.urlsafe_b64encode(kdf.derive(master_password.encode()))
return Fernet(key)
def encrypt(self, data):
"""Encrypt the data."""
return self.fernet.encrypt(data.encode()).decode()
def decrypt(self, encrypted_data):
"""Decrypt the data."""
return self.fernet.decrypt(encrypted_data.encode()).decode()

0
src/gui/__init__.py Normal file
View file

44
src/gui/app.py Normal file
View file

@ -0,0 +1,44 @@
import tkinter as tk
from gui.login_screen import LoginScreen
from gui.main_screen import MainScreen
from crypto.encryption import EncryptionManager
from utils.storage import StorageManager
class PasswordManager:
def __init__(self):
self.root = tk.Tk()
self.root.title("KeyKoala")
self.root.geometry("600x800")
self.storage_manager = StorageManager()
self.encryption_manager = None
self.current_screen = None
self.setup_gui()
def setup_gui(self):
self.show_login_screen()
def show_login_screen(self):
if self.current_screen:
self.current_screen.destroy()
self.current_screen = LoginScreen(self.root, self.on_login)
def show_main_screen(self):
if self.current_screen:
self.current_screen.destroy()
self.current_screen = MainScreen(
self.root,
self.encryption_manager,
self.storage_manager
)
def on_login(self, master_password):
self.encryption_manager = EncryptionManager(
master_password,
self.storage_manager.get_salt()
)
self.show_main_screen()
def run(self):
self.root.mainloop()

24
src/gui/login_screen.py Normal file
View file

@ -0,0 +1,24 @@
import tkinter as tk
from tkinter import ttk
class LoginScreen(ttk.Frame):
def __init__(self, parent, login_callback):
super().__init__(parent, padding="20")
self.login_callback = login_callback
self.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
self.setup_gui()
def setup_gui(self):
ttk.Label(self, text="Master Password:").grid(row=0, column=0, pady=10)
self.password_entry = ttk.Entry(self, show="*")
self.password_entry.grid(row=0, column=1, pady=10)
ttk.Button(
self,
text="Login",
command=self.handle_login
).grid(row=1, column=0, columnspan=2, pady=10)
def handle_login(self):
master_password = self.password_entry.get()
self.login_callback(master_password)

111
src/gui/main_screen.py Normal file
View file

@ -0,0 +1,111 @@
import tkinter as tk
from tkinter import ttk, messagebox
import pyperclip
class MainScreen(ttk.Frame):
def __init__(self, parent, encryption_manager, storage_manager):
super().__init__(parent, padding="20")
self.encryption_manager = encryption_manager
self.storage_manager = storage_manager
self.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
self.setup_gui()
def setup_gui(self):
self.create_entry_fields()
self.create_treeview()
self.create_context_menu()
self.load_entries()
def create_entry_fields(self):
ttk.Label(self, text="Site:").grid(row=0, column=0, pady=5)
self.site_entry = ttk.Entry(self)
self.site_entry.grid(row=0, column=1, pady=5)
ttk.Label(self, text="Username:").grid(row=1, column=0, pady=5)
self.username_entry = ttk.Entry(self)
self.username_entry.grid(row=1, column=1, pady=5)
ttk.Label(self, text="Password:").grid(row=2, column=0, pady=5)
self.password_entry = ttk.Entry(self, show="*")
self.password_entry.grid(row=2, column=1, pady=5)
ttk.Button(
self,
text="Add Entry",
command=self.add_entry
).grid(row=3, column=0, columnspan=2, pady=10)
def create_treeview(self):
columns = ("Site", "Username")
self.tree = ttk.Treeview(self, columns=columns, show="headings")
for col in columns:
self.tree.heading(col, text=col)
self.tree.column(col, width=100)
self.tree.grid(row=4, column=0, columnspan=2, pady=10)
def create_context_menu(self):
self.popup_menu = tk.Menu(self, tearoff=0)
self.popup_menu.add_command(
label="Copy Password",
command=self.copy_password
)
self.popup_menu.add_command(
label="Delete Entry",
command=self.delete_entry
)
self.tree.bind("<Button-3>", self.show_popup_menu)
def show_popup_menu(self, event):
item = self.tree.identify_row(event.y)
if item:
self.tree.selection_set(item)
self.popup_menu.post(event.x_root, event.y_root)
def add_entry(self):
site = self.site_entry.get()
username = self.username_entry.get()
password = self.password_entry.get()
if not all([site, username, password]):
messagebox.showerror("Error", "All fields are required!")
return
encrypted_password = self.encryption_manager.encrypt(password)
self.storage_manager.add_entry(site, username, encrypted_password)
self.load_entries()
self.clear_entries()
def clear_entries(self):
self.site_entry.delete(0, tk.END)
self.username_entry.delete(0, tk.END)
self.password_entry.delete(0, tk.END)
def load_entries(self):
self.tree.delete(*self.tree.get_children())
entries = self.storage_manager.get_all_entries()
for site, data in entries.items():
self.tree.insert("", tk.END, values=(site, data["username"]))
def copy_password(self):
selected = self.tree.selection()
if not selected:
return
site = self.tree.item(selected[0])["values"][0]
encrypted_password = self.storage_manager.get_password(site)
decrypted_password = self.encryption_manager.decrypt(encrypted_password)
pyperclip.copy(decrypted_password)
messagebox.showinfo("Success", "Password copied to clipboard!")
def delete_entry(self):
selected = self.tree.selection()
if not selected:
return
site = self.tree.item(selected[0])["values"][0]
self.storage_manager.delete_entry(site)
self.load_entries()

8
src/main.py Normal file
View file

@ -0,0 +1,8 @@
from gui.app import PasswordManager
def main():
app = PasswordManager()
app.run()
if __name__ == "__main__":
main()

0
src/utils/__init__.py Normal file
View file

68
src/utils/storage.py Normal file
View file

@ -0,0 +1,68 @@
import json
import os
import base64
import secrets
class StorageManager:
def __init__(self, data_file="passwords.enc"):
self.data_file = data_file
self.config_file = "config.json"
self.passwords = {}
self.salt = self._get_or_create_salt()
self._load_data()
def _get_or_create_salt(self):
"""Generate or retrieve the salt from config file."""
if os.path.exists(self.config_file):
try:
with open(self.config_file, "r") as f:
config = json.load(f)
return base64.b64decode(config["salt"])
except Exception:
return self._create_new_salt()
return self._create_new_salt()
def _create_new_salt(self):
"""Generate a new salt and save it to config."""
salt = secrets.token_bytes(32) # 32 bytes = 256 bits
config = {"salt": base64.b64encode(salt).decode()}
with open(self.config_file, "w") as f:
json.dump(config, f)
return salt
def get_salt(self):
"""Return the current salt."""
return self.salt
def _load_data(self):
if os.path.exists(self.data_file):
try:
with open(self.data_file, "r") as f:
return json.load(f)
except Exception:
return {}
return {}
def _save_data(self):
with open(self.data_file, "w") as f:
json.dump(self.passwords, f)
def add_entry(self, site, username, encrypted_password):
self.passwords[site] = {
"username": username,
"password": encrypted_password
}
self._save_data()
def get_all_entries(self):
return self.passwords
def get_password(self, site):
return self.passwords[site]["password"]
def delete_entry(self, site):
if site in self.passwords:
del self.passwords[site]
self._save_data()