PassKeeper 🔐
A self-hosted, zero-knowledge password manager — web app and browser extension. Modelled after LastPass, built on Flask + MySQL + Web Crypto API.
Your master password and decrypted vault data never leave your browser. The server stores only encrypted blobs it cannot read.
Features
Web App
- Zero-knowledge encryption — AES-256-GCM client-side, 600k-iteration PBKDF2 vault key derivation
- 7 item types — Passwords, Secure Notes, Payment Cards, Bank Accounts, Addresses, Identities, Passkeys
- TOTP/2FA support — per-site TOTP codes stored inside the encrypted vault blob; live 6-digit display with countdown
- Folder organisation — create, rename, delete folders; filter vault by folder
- Item sharing — zero-knowledge ECDH P-256 re-encryption; share with any registered user
- Emergency access — configurable wait-timer access grant for a trusted contact
- Security dashboard — weak, reused, and old password detection + HaveIBeenPwned k-anonymity breach check (passwords never transmitted)
- Account MFA — TOTP-based login verification (Google Authenticator / Authy)
- Master password change — atomic zero-knowledge re-encryption of the entire vault
- Account recovery — 128-bit recovery code; server never stores it
- Audit log — server-side trail of all create/edit/delete actions; no sensitive data ever logged
- Encrypted item names — item names stored as AES-256-GCM ciphertext; server holds only the item type as a label
- Responsive layout — phone, tablet, laptop, and large desktop breakpoints; collapsible sidebar
Browser Extension (Chrome / Edge — Manifest V3)
- Autofill — detects login forms; injects icon into username and password fields only (scored heuristic, not all text inputs)
- Suggestion dropdown — anchored below the focused field; filters as you type; keyboard navigation (↑↓ Enter); fills with one click
- Smart domain matching — matches vault items by domain name, handles bare domains (
github.com) and subdomains - Auto-save banner — prompts to save or update credentials on form submit; duplicate detection; "Never for this site" blocklist
- Inline password generator — length slider, charset toggles, strength indicator; fully CSPRNG (
crypto.getRandomValuesthroughout) - TOTP live display — 6-digit code + countdown timer per item in the popup
- SSO bridge — log in once on the web app; extension picks up the session automatically
- Idle lock — configurable auto-lock timeout (1 / 5 / 10 / 30 min / Never)
- Security — decrypted vault data stored in
chrome.storage.sessiononly (memory-only, never written to disk)
Security Architecture
Master Password
│
├─ PBKDF2(email, 100k iter) ──► authHash ──► POST /api/auth/login
│ │
│ Argon2id(authHash) stored in DB
│
└─ PBKDF2(enc_key_salt, 600k iter) ──► vaultKey (stays in browser memory only)
│
AES-256-GCM encrypt
│
enc_data + iv ──► POST /api/vault
enc_name + iv_name (item name, encrypted separately)
The server is blind to all vault content. A database breach exposes only encrypted ciphertext.
Tech Stack
| Layer | Technology |
|---|---|
| Backend | Python 3.12, Flask 3.x |
| Database | MySQL 8.x |
| Frontend | Vanilla JS, Web Crypto API, Jinja2 |
| Auth | Argon2id + PBKDF2 + JWT (HS256) |
| Encryption | AES-256-GCM (client-side) |
| Extension | Chrome Manifest V3 |
| Web server | Nginx + Gunicorn + systemd |
| Rate limiting | Flask-Limiter + Redis |
| TLS | Let's Encrypt / Certbot |
Getting Started
Prerequisites
- Python 3.12+
- MySQL 8.x
- Node.js is not required — no build step
Development (Windows)
# 1. Clone and set up virtual environment
git clone https://github.com/yourname/passkeeper
cd passkeeper
python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txt
# 2. Configure environment
copy .env.example .env
Edit .env:
FLASK_ENV=development
SECRET_KEY=your-secret-key
JWT_SECRET_KEY=your-jwt-secret
MYSQL_HOST=localhost
MYSQL_USER=passkeeper
MYSQL_PASSWORD=yourpassword
MYSQL_DB=passkeeper
TOTP_ENCRYPTION_KEY=64-char-hex-string # python -c "import secrets; print(secrets.token_hex(32))"
# 3. Create database
mysql -u root -p -e "CREATE DATABASE passkeeper CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mysql -u root -p -e "CREATE USER 'passkeeper'@'localhost' IDENTIFIED BY 'yourpassword';"
mysql -u root -p -e "GRANT ALL PRIVILEGES ON passkeeper.* TO 'passkeeper'@'localhost'; FLUSH PRIVILEGES;"
# 4. Create tables
python reset_db.py
# 5. Run
python run.py
Open http://localhost:5000.
Load the Extension (Chrome)
- Open
chrome://extensions/ - Enable Developer mode (top-right toggle)
- Click Load unpacked → select the
extension/folder - Reload the extension after any JS/CSS changes
Content script logs appear in the page's DevTools console. Background service worker logs are at chrome://extensions/ → PassKeeper → "Service Worker".
Production Deployment
1. Server dependencies
sudo apt update && sudo apt install python3.12 python3.12-venv mysql-server nginx \
certbot python3-certbot-nginx redis-server
2. Application setup
cd /var/www/passkeeper
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # set production values
flask db upgrade # run all migrations
3. systemd service
Copy scripts/passkeeper.service to /etc/systemd/system/passkeeper.service, then:
sudo systemctl daemon-reload
sudo systemctl enable --now passkeeper
journalctl -xeu passkeeper.service
Key flags in the service unit: --preload (single app import, lower memory), WatchdogSec=60s, Restart=on-failure.
4. Nginx
Copy scripts/passkeeper-nginx.conf to /etc/nginx/sites-available/passkeeper, update server_name, then:
sudo ln -s /etc/nginx/sites-available/passkeeper /etc/nginx/sites-enabled/
sudo certbot --nginx -d yourdomain.com
sudo nginx -t && sudo systemctl reload nginx
The Nginx config uses an upstream block with proxy_next_upstream for zero-downtime Gunicorn restarts, dual rate-limit zones (auth endpoints and general API), and Cache-Control: immutable for static assets.
5. Schema migrations (future updates)
flask db upgrade
sudo systemctl reload passkeeper
Environment Variables
| Variable | Description | Example |
|---|---|---|
SECRET_KEY |
Flask session secret | 64-char random hex |
JWT_SECRET_KEY |
JWT signing secret | 64-char random hex |
MYSQL_HOST |
MySQL host | localhost |
MYSQL_USER |
MySQL username | passkeeper |
MYSQL_PASSWORD |
MySQL password | — |
MYSQL_DB |
Database name | passkeeper |
TOTP_ENCRYPTION_KEY |
Server-side AES key for TOTP secrets | 64-char hex |
RATELIMIT_STORAGE_URI |
Redis URI for rate limiting | redis://127.0.0.1:6379/0 |
CORS_ORIGINS |
Allowed CORS origins | https://yourdomain.com |
Generate secrets:
python -c "import secrets; print(secrets.token_hex(32))"
Project Structure
passkeeper/
├── app/ # Flask application
│ ├── models/ # SQLAlchemy models
│ ├── routes/ # API blueprints (auth, vault, folders, sharing, emergency)
│ ├── services/ # Auth service (Argon2id, JWT, TOTP encryption)
│ ├── static/js/ # Client-side crypto + vault UI
│ └── templates/ # Jinja2 HTML templates
├── extension/ # Chrome extension (Manifest V3)
│ ├── popup/ # Popup UI (HTML + CSS + JS)
│ ├── content/ # Content script (form detection, autofill icon, dropdown)
│ ├── shared/ # Shared crypto (vault key, encryptName/decryptName)
│ ├── bridge/ # SSO bridge (web app ↔ extension session sync)
│ └── background.js # Service worker (badges, message relay, idle lock)
├── migrations/ # Alembic migration scripts
├── scripts/ # Nginx config, systemd unit, backup scripts
├── reset_db.py # Dev-only: drop + recreate all tables
├── requirements.txt
├── run.py # Development server entry point
└── wsgi.py # Gunicorn entry point
API Overview
All vault and folder endpoints require Authorization: Bearer <access_token>.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/auth/register |
Create account |
| POST | /api/auth/login |
Authenticate; returns tokens or MFA challenge |
| POST | /api/auth/mfa/verify |
Complete MFA step |
| POST | /api/auth/refresh |
Rotate refresh token |
| POST | /api/auth/logout |
Blacklist both tokens |
| GET | /api/vault |
List all encrypted vault items |
| POST | /api/vault |
Create item (enc_data, iv, enc_name, iv_name) |
| PUT | /api/vault/<id> |
Update item |
| DELETE | /api/vault/<id> |
Delete item |
| GET | /api/folders |
List folders |
| POST | /api/sharing |
Share item (ECDH re-encryption) |
| POST | /api/emergency |
Create emergency access grant |
| POST | /api/auth/change-password |
Atomic vault re-encryption on password change |
| POST | /api/auth/recover |
Account recovery (one-time use) |
Backup
Automated MySQL backups with 30-day retention:
# Install cron job
sudo cp scripts/passkeeper-logrotate /etc/logrotate.d/passkeeper
crontab scripts/backup.cron
Manual backup:
bash scripts/backup_db.sh
Licence
MIT — see LICENSE.