# PassKeeper 🔐 A self-hosted, zero-knowledge password manager — web app and Chrome/Firefox extension. Your master password and decrypted vault data **never leave your browser**. --- ## Features ### Web App - **Zero-knowledge encryption** — AES-256-GCM client-side; 600k-iteration PBKDF2 vault key - **7 item types** — Passwords, Secure Notes, Cards, Bank Accounts, Addresses, Identities, Passkeys - **Vault item tags** — comma-separated tags stored inside the encrypted blob; sidebar tag filter; live badge preview in modal; no schema change required - **Collapsible folder groups** — click any folder header in the vault list to collapse/expand; item count badge and chevron indicator; state persists across re-renders - **TOTP / 2FA** — per-site TOTP codes stored in `plain.totp_uri`; live 6-digit display with countdown - **Folder organisation** — create, rename, delete; filter vault by folder - **Item sharing** — ECDH P-256 zero-knowledge re-encryption; item name encrypted with shared key — server never sees it - **Emergency access** — configurable wait-timer access grant for a trusted contact - **Security dashboard** — weak / reused / old / **no 2FA saved** / **HaveIBeenPwned breach check** (k-anonymity — passwords never transmitted) - **Import / Export** — encrypted JSON backup; CSV export (plaintext, handle carefully); import from Chrome, Bitwarden, and 1Password CSV formats (RFC 4180 compliant parser) - **Account MFA** — TOTP-based login (Google Authenticator / Authy); single-use code enforcement prevents replay attacks - **Passkeys / WebAuthn** — register device biometrics or hardware keys as a sign-in method; master password still required to unlock vault (zero-knowledge preserved); manage passkeys in Account Settings - **Master password change** — atomic zero-knowledge re-encryption of entire vault including item names - **Account recovery** — 128-bit recovery code; server never stores it; challenge-response proof prevents forgery - **Audit log** — server-side trail of all create/edit/delete/import/export actions; no plaintext names ever logged - **Encrypted item names** — `enc_name`/`iv_name`; server holds only the item type as a label - **Browser history** — back/forward button works for all views (`history.pushState`) - **Web-app auto-lock** — configurable inactivity timeout (5/10/15/30/60 min or Never); stored per browser in `localStorage` - **Clipboard auto-clear** — sensitive copies cleared after 30 seconds - **Responsive layout** — phone, tablet, laptop, large desktop; collapsible sidebar ### Browser Extension (Chrome / Edge — Manifest V3 + Firefox — Manifest V2) - **Autofill** — detects login forms; injects icon into username and password fields only (scored heuristic, not all inputs) - **Smart domain matching** — matches by domain name; handles bare domains (`github.com`) and subdomains - **Suggestion dropdown** — filters as you type; keyboard navigation (↑↓ Enter); one-click fill - **Collapsible folder groups** — "All items" tab groups by folder with collapse/expand toggle matching the web app UX - **Favorites tab** — items tagged `favorite` in the web app appear in a dedicated tab; marked with ★ - **Tag display** — purple badge pills on item rows; tags sourced from `plain.tags[]` - **Three-dot flyout menu** — contextual actions: Open URL / Copy username / Copy password; built dynamically from what the item has - **Dedicated copy-username button** — person icon alongside copy-password in every item row - **Auto-save banner** — save/update credentials on form submit; duplicate detection; "Never for this site" blocklist - **Pending-save badge** — red `!` on toolbar icon when a credential is waiting to be saved - **Inline password generator** — fully CSPRNG (`crypto.getRandomValues` throughout, `Math.random` never called) - **TOTP live display** — 6-digit code + countdown per item - **SSO bridge** — log in once on the web app; extension picks up the session - **Idle lock** — configurable auto-lock (1/5/10/30 min or Never) - **Clipboard auto-clear** — passwords/usernames cleared from clipboard after 30 seconds - **Keyboard shortcut** — `Ctrl+Shift+L` / `Cmd+Shift+L` to open popup (customisable at `chrome://extensions/shortcuts`) - **Firefox compatible** — separate MV2 manifest + background script; same popup/content/bridge code --- ## Security Architecture ``` Master Password │ ├─ PBKDF2(email, 100k iter) ──► authHash ──► POST /api/auth/login │ Argon2id(authHash) stored in DB │ (transparently rehashed if params upgraded) │ └─ PBKDF2(enc_key_salt, 600k iter) ──► vaultKey (browser memory only) │ AES-256-GCM encrypt │ enc_data + iv (item payload + tags) enc_name + iv_name (item name) │ POST /api/vault ──► Server stores ciphertext only Sharing: ECDH(Alice_priv, Bob_pub) ──► sharedKey ──► AES-256-GCM(enc_data + enc_name) Server stores only ciphertext — cannot read item content or item name ``` ### Security hardening highlights - **TOTP replay prevention** — each 6-digit code is single-use (120s window); recorded in `totp_used_codes` table - **Recovery proof** — `enc_key_salt` not returned by server during recovery; client must decrypt the recovery blob to prove code possession - **folder_id ownership** — all create/update/import operations validate folder belongs to current user - **Audit log privacy** — item names and shared item names never appear in server-side audit logs - **Extension fingerprinting** — `web_accessible_resources: []` blocks external pages from probing extension files - **Passkey / WebAuthn** — FIDO2 assertion proves identity to the server without a password; vault key still derived from master password client-side; `sign_count` updated on each use for clone detection A database breach exposes only encrypted ciphertext. The server cannot read vault names, passwords, tags, or shared item names. --- ## 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) + WebAuthn (FIDO2) | | Encryption | AES-256-GCM (client-side) | | Extension | Chrome MV3 / Firefox MV2 | | Web server | Nginx + Gunicorn + systemd | | Rate limiting | Flask-Limiter + Redis | | TLS | Let's Encrypt / Certbot | --- ## Getting Started ### Prerequisites - Python 3.12+ - MySQL 8.x - No Node.js or build step required ### Development (Windows) ```bash git clone https://github.com/yourname/passkeeper cd passkeeper python -m venv .venv .venv\Scripts\activate pip install -r requirements.txt copy .env.example .env ``` Edit `.env`: ```env FLASK_ENV=development SECRET_KEY=<64-char hex> JWT_SECRET_KEY=<64-char hex> MYSQL_HOST=localhost MYSQL_USER=passkeeper MYSQL_PASSWORD=yourpassword MYSQL_DB=passkeeper TOTP_ENCRYPTION_KEY=<64-char hex> ``` ```bash 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'; GRANT ALL ON passkeeper.* TO 'passkeeper'@'localhost'; FLUSH PRIVILEGES;" python reset_db.py python run.py ``` Generate secrets: ```bash python -c "import secrets; print(secrets.token_hex(32))" ``` ### Load the Extension (Chrome) 1. `chrome://extensions/` → Enable **Developer mode** 2. **Load unpacked** → select `extension/` folder 3. Reload after any JS/CSS changes ### Load the Extension (Firefox) 1. `about:debugging` → **This Firefox** → **Load Temporary Add-on** 2. Select `extension/manifest.firefox.json` --- ## 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 ```bash sudo cp scripts/passkeeper.service /etc/systemd/system/ sudo systemctl daemon-reload && sudo systemctl enable --now passkeeper ``` ### 4. Nginx ```bash sudo cp scripts/passkeeper-nginx.conf /etc/nginx/sites-available/passkeeper 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 ``` ### 5. Future schema migrations ```bash flask db upgrade && sudo systemctl reload passkeeper ``` --- ## Environment Variables | Variable | Description | |---|---| | `SECRET_KEY` | Flask session secret (64-char hex) | | `JWT_SECRET_KEY` | JWT signing secret (64-char hex) | | `MYSQL_HOST` / `MYSQL_USER` / `MYSQL_PASSWORD` / `MYSQL_DB` | Database | | `TOTP_ENCRYPTION_KEY` | Server-side AES key for TOTP secrets (64-char hex) | | `RATELIMIT_STORAGE_URI` | Redis URI — required in production (`redis://127.0.0.1:6379/0`) | | `CORS_ORIGINS` | Allowed origins (`*` in dev, domain in prod) | | `WEBAUTHN_RP_ID` | Passkey relying party ID — effective domain, no scheme (`pwkeeper.ngodanguyen.tech`) | | `WEBAUTHN_RP_NAME` | Passkey relying party display name (`PassKeeper`) | | `WEBAUTHN_ORIGINS` | Comma-separated allowed origins for WebAuthn (`https://pwkeeper.ngodanguyen.tech`) | --- ## Project Structure ``` passkeeper/ ├── app/ # Flask application │ ├── models/ # SQLAlchemy models │ ├── routes/ # API blueprints (auth, vault, folders, sharing, emergency) │ ├── services/ # Auth (Argon2id, JWT, TOTP encryption, TOTP replay helpers) │ ├── static/js/ # Client-side crypto + vault UI │ └── templates/ # Jinja2 templates ├── extension/ # Browser extension │ ├── popup/ # Popup UI │ ├── content/ # Content script (autofill, field detection) │ ├── shared/ # Shared crypto + Firefox polyfill │ ├── bridge/ # SSO bridge │ ├── background.js # Chrome MV3 service worker │ ├── background.firefox.js # Firefox MV2 background page │ ├── manifest.json # Chrome/Edge MV3 │ └── manifest.firefox.json # Firefox MV2 ├── migrations/ # Alembic migration scripts ├── scripts/ # Nginx, systemd, backup └── requirements.txt ``` --- ## API Overview All vault/folder endpoints require `Authorization: Bearer `. | Method | Endpoint | Description | |---|---|---| | POST | `/api/auth/register` | Create account | | POST | `/api/auth/login` | Authenticate | | POST | `/api/auth/mfa/verify` | Complete MFA (single-use code) | | POST | `/api/auth/refresh` | Rotate tokens | | POST | `/api/auth/logout` | Blacklist tokens | | GET | `/api/vault` | List encrypted items | | POST | `/api/vault` | Create item (`enc_data`, `iv`, `enc_name`, `iv_name`, tags in payload) | | PUT | `/api/vault/` | Update item | | DELETE | `/api/vault/` | Delete item | | GET | `/api/vault/export` | Download encrypted JSON backup | | POST | `/api/vault/import` | Bulk import; returns `{ imported, skipped }` | | GET/POST | `/api/folders` | List / create folders | | POST | `/api/sharing` | Share item — sends `enc_name`/`iv_name` for ZK name | | POST | `/api/emergency` | Create emergency access grant | | POST | `/api/auth/change-password` | Atomic vault re-encryption | | POST | `/api/auth/recover` | Account recovery (one-time) | | POST | `/api/webauthn/register/begin` | Start passkey registration | | POST | `/api/webauthn/register/complete` | Finish passkey registration | | POST | `/api/webauthn/authenticate/begin` | Start passkey login | | POST | `/api/webauthn/authenticate/complete` | Complete passkey login → tokens | | GET | `/api/webauthn/credentials` | List registered passkeys | | DELETE | `/api/webauthn/credentials/` | Remove a passkey | --- ## Item Tagging Tags are stored as `plain.tags: string[]` inside the encrypted vault blob — the server never sees them and no schema change is required. **Web app:** Add tags in the item edit modal (comma-separated). Tags appear as purple badge pills on item rows. A Tags section in the sidebar lets you filter by any tag. Search respects the active tag filter. **Extension:** Tags appear as `.pk-tag` badges on item rows. Items tagged `favorite` appear in the **Favorites** tab and show a ★ in the site label. --- ## Backup ```bash # 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`.