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)
- Proactive health notifications — sidebar badge and dismissible banner on vault load when breached, weak, or reused passwords are detected; HIBP runs in the background without blocking the UI; results cached so opening the Security tab is instant
- 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 (YubiKey, cross-device QR) as a sign-in method; master password still required to unlock vault (zero-knowledge preserved); manage passkeys in Account Settings; transport type shown per credential (📱 Device / 🔑 Security key)
- 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); also locks immediately when tab is hidden (screen lock, minimize) for longer than the timeout
- 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
favoritein 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.getRandomValuesthroughout,Math.randomnever 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+Lto open popup (customisable atchrome://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_codestable - Recovery proof —
enc_key_saltnot 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_countupdated on each use for clone detection - Password age tracking —
password_changed_atstored inside encrypted blob; security dashboard uses actual password change date, not item creation date - Emergency access stale snapshot detection — server flags provisioned snapshots created before the
enc_namefix; UI prompts grantor to re-provision
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)
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:
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>
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:
python -c "import secrets; print(secrets.token_hex(32))"
Load the Extension (Chrome)
chrome://extensions/→ Enable Developer mode- Load unpacked → select
extension/folder - Reload after any JS/CSS changes
Load the Extension (Firefox)
about:debugging→ This Firefox → Load Temporary Add-on- Select
extension/manifest.firefox.json
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
sudo cp scripts/passkeeper.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now passkeeper
4. Nginx
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
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 <access_token>.
| 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/<id> |
Update item |
| DELETE | /api/vault/<id> |
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/<id> |
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
# 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.