# 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.getRandomValues` throughout) - **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.session` only (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) ```bash # 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`: ```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))" ``` ```bash # 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) 1. Open `chrome://extensions/` 2. Enable **Developer mode** (top-right toggle) 3. Click **Load unpacked** → select the `extension/` folder 4. 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 ```bash sudo apt update && sudo apt install python3.12 python3.12-venv mysql-server nginx \ certbot python3-certbot-nginx redis-server ``` ### 2. Application setup ```bash 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: ```bash 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: ```bash 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) ```bash 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: ```bash 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 `. | 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/` | Update item | | DELETE | `/api/vault/` | 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: ```bash # Install cron job sudo cp scripts/passkeeper-logrotate /etc/logrotate.d/passkeeper crontab scripts/backup.cron ``` Manual backup: ```bash bash scripts/backup_db.sh ``` --- ## Licence MIT — see `LICENSE`.