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