init commit of KeyKoala
This commit is contained in:
parent
751d67d75f
commit
ef79fce1d6
13 changed files with 420 additions and 0 deletions
135
README.md
Normal file
135
README.md
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
# 🐨 KeyKoala
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
A secure, lightweight, and user-friendly password manager built with Python.
|
||||||
|
|
||||||
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
[](https://www.python.org/downloads/)
|
||||||
|
[](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>
|
||||||
|
```
|
||||||
BIN
assets/images/keykoala_logo.png
Normal file
BIN
assets/images/keykoala_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 185 KiB |
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
cryptography>=3.4.7
|
||||||
|
pyperclip>=1.8.2
|
||||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
0
src/crypto/__init__.py
Normal file
0
src/crypto/__init__.py
Normal file
28
src/crypto/encryption.py
Normal file
28
src/crypto/encryption.py
Normal 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
0
src/gui/__init__.py
Normal file
44
src/gui/app.py
Normal file
44
src/gui/app.py
Normal 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
24
src/gui/login_screen.py
Normal 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
111
src/gui/main_screen.py
Normal 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
8
src/main.py
Normal 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
0
src/utils/__init__.py
Normal file
68
src/utils/storage.py
Normal file
68
src/utils/storage.py
Normal 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()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue