# PassKeeper — Password Manager Web App & Browser Extension ## Project Overview A full-featured password manager web app and browser extension modelled after LastPass. Users can store, organise, and autofill credentials securely with a zero-knowledge architecture. --- ## Tech Stack ### Development (Windows) - **Backend:** Python 3.12, Flask 3.x - **Database:** MySQL 8.x - **Frontend:** Vanilla JS (Web Crypto API) + Jinja2 templates - **Dev server:** `python run.py` ### Production (Ubuntu) - **Web server:** Nginx (reverse proxy, TLS termination) - **WSGI server:** Gunicorn - **Process manager:** systemd - **Database:** MySQL 8.x - **TLS:** Let's Encrypt / Certbot --- ## Architecture ``` Browser Extension <──────────────────────────────────────┐ Web App <──> Nginx ──> Gunicorn ──> Flask App │ │ │ MySQL DB │ │ REST API (JSON, HTTPS) ───────┘ ``` ### Project File Structure ``` passkeeper/ ├── app/ │ ├── __init__.py # App factory, blueprints, CSRF exemptions, security headers │ ├── config.py # DevelopmentConfig / ProductionConfig │ ├── models/ │ │ ├── user.py # Argon2id, TOTP encrypted, ECDH keys, recovery │ │ ├── vault_item.py # enc_data, iv, enc_name, iv_name │ │ ├── folder.py │ │ ├── token_blacklist.py # JWT revocation (jti + expires_at) │ │ ├── shared_item.py # ECDH-encrypted cross-user shares │ │ ├── emergency_access.py # State machine │ │ └── audit_log.py │ ├── routes/ │ │ ├── auth.py # Register, login, MFA, logout, refresh, change-password, recovery │ │ ├── vault.py # CRUD + GET /export + POST /import │ │ ├── folders.py │ │ ├── sharing.py │ │ └── emergency.py │ ├── services/ │ │ └── auth_service.py # Argon2id, JWT, blacklist, @require_jwt, TOTP encrypt/decrypt │ ├── static/ │ │ ├── css/app.css # Full responsive stylesheet; collapsible group styles; tag badge styles │ │ └── js/ │ │ ├── crypto.js # deriveAuthHash, deriveVaultKey, encryptItem, decryptItem, │ │ │ # encryptName, decryptName, generateSalt │ │ ├── auth.js # Login/register + TOTP MFA + VaultSession │ │ ├── recover.js │ │ ├── sharing.js # ECDH P-256 │ │ └── vault.js # Full vault UI — see "Key Features" below │ └── templates/vault/ │ └── index.html # Tags field, Auto-Lock setting, Import/Export view + sidebar ├── extension/ │ ├── manifest.json # MV3 (Chrome/Edge); Ctrl+Shift+L keyboard shortcut │ ├── manifest.firefox.json # MV2 (Firefox) │ ├── background.js # Chrome SW: badges, pending-save "!" badge, CLEAR_SAVE_BADGE │ ├── background.firefox.js # Firefox: in-memory session shim, setTimeout idle lock │ ├── shared/ │ │ ├── crypto.js # PBKDF2+AES-GCM; encryptName/decryptName; extractable key │ │ └── browser-polyfill.js # chrome=browser alias for Firefox content scripts │ ├── popup/ │ │ ├── popup.html # Tabs: All relevant / All items / Favorites / Recents │ │ ├── popup.css # .pk-group-* collapsible styles; .pk-tag badge; .pk-flyout menu │ │ └── popup.js # Vault+folder fetch; collapsible groups; Favorites tab; │ │ # tag display; clipboard auto-clear; CSPRNG generator; │ │ # save badge clear; three-dot flyout; copy-username button │ ├── content/ │ │ └── content.js # _isLikelyUsernameField; _normaliseUrl; debounced input (150ms); │ │ # MutationObserver guard; vault_items_cs from chrome.storage.session │ ├── bridge/ │ │ └── bridge.js # SSO bridge (vault domain only) │ └── icons/ ├── migrations/versions/ │ ├── 71d7158dd3b9_add_audit_log_tables_fix_sharing_public_.py │ ├── a1b2c3d4e5f6_encrypt_totp_secret_at_rest.py │ ├── b2c3d4e5f6a7_add_account_recovery_columns.py │ └── c3d4e5f6a7b8_encrypt_vault_item_name.py # adds enc_name + iv_name ├── scripts/ │ ├── reencrypt_totp_secrets.py │ ├── backup_db.sh / backup.cron / passkeeper-logrotate │ ├── passkeeper-nginx.conf / passkeeper.service ├── reset_db.py ├── requirements.txt ├── wsgi.py / run.py └── CLAUDE.md ``` --- ## Database Schema (MySQL) ```sql -- Users CREATE TABLE users ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, master_hash VARCHAR(255) NOT NULL, -- Argon2id(authHash) enc_key_salt VARCHAR(64) NOT NULL, created_at / last_login DATETIME, totp_secret VARCHAR(255), -- AES-256-GCM ciphertext totp_iv VARCHAR(64), totp_enabled TINYINT(1) DEFAULT 0, sharing_public_key VARCHAR(128), sharing_private_key_enc TEXT, sharing_private_key_iv VARCHAR(64), recovery_enc_salt VARCHAR(128), recovery_iv VARCHAR(64) ); -- Vault Items CREATE TABLE vault_items ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL, folder_id INT UNSIGNED, item_type VARCHAR(20) NOT NULL DEFAULT 'password', name VARCHAR(255) NOT NULL, -- server label only (item type string) enc_data TEXT NOT NULL, -- AES-256-GCM ciphertext of payload + tags iv VARCHAR(64) NOT NULL, enc_name TEXT, -- AES-256-GCM ciphertext of item name (nullable) iv_name VARCHAR(64), created_at DATETIME DEFAULT NOW(), updated_at DATETIME DEFAULT NOW() ON UPDATE NOW(), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (folder_id) REFERENCES folders(id) ON DELETE SET NULL ); -- (folders, token_blacklist, shared_items, emergency_access, audit_logs — unchanged) ``` --- ## Migration History | Revision | Description | | -------------- | ------------------------------------------- | | `71d7158dd3b9` | Add audit_logs; fix sharing_public_key type | | `a1b2c3d4e5f6` | Encrypt TOTP secret at rest | | `b2c3d4e5f6a7` | Add account recovery columns | | `c3d4e5f6a7b8` | Add enc_name + iv_name to vault_items | --- ## Security Model - **Zero-knowledge:** master password never sent to server - `authHash = PBKDF2(masterPassword, email, 100k iter)` → auth only - `vaultKey = PBKDF2(masterPassword, enc_key_salt, 600k iter)` → browser memory only - All vault data (payload + name + tags + advanced settings) encrypted client-side (AES-256-GCM) - **Item name:** `enc_name`/`iv_name`; server `name` column = item type only - **Tags:** `plain.tags: string[]` inside `enc_data`; server never sees them - **Advanced Settings:** `plain.autofill`, `plain.autologin`, `plain.reprompt` — boolean fields inside `enc_data`; zero-knowledge like tags - **Argon2id:** double-hashes `authHash` server-side - **JWT:** HS256, 15 min access / 7 day refresh, JTI blacklisted on logout - **MFA:** TOTP secret AES-256-GCM encrypted at rest - **Sharing:** ECDH P-256 zero-knowledge re-encryption - **Password generator:** fully CSPRNG (`_cryptoRandInt` rejection-sampling) - **Decrypted vault data:** `chrome.storage.session` only — never to disk - **Clipboard auto-clear:** 30 s after any password/username copy (web + extension) - **Breach detection:** HIBP k-anonymity — only 5-char SHA-1 prefix transmitted --- ## Extension Storage Architecture | Data | Storage | Reason | | -------------------------------------------------------------------------------------- | ----------------------------------- | ------------------------------------- | | `access_token`, `vault_key_jwk`, `vault_items`, `vault_items_cs` | `chrome.storage.session` | Memory-only, cleared on browser close | | `refresh_token`, `enc_key_salt`, `pending_save`, `save_blocklist`, `idle_lock_seconds` | `chrome.storage.local` | Persists across restarts | | Web-app session timeout | `localStorage` (`web_idle_minutes`) | Per-browser preference | ### Critical: `vault_items_cs` is in `session`, NOT `local` Requires Chrome 111+. `content.js` `onChanged` listener watches `area === 'session'`. Do not revert — reverting persists decrypted passwords to disk. ### Pending-save badge `background.js` sets a red `"!"` badge on `SAVE_CREDENTIALS`. Cleared via `CLEAR_SAVE_BADGE` when user acts on the save prompt in the popup. --- ## Key Implementation Details ### Vault item tags - Stored as `plain.tags: string[]` inside `enc_data`. No schema change ever needed. - **Web app:** `_parseTags(str)` → comma-split, lowercase, dedup, sort. `_allTags()` collects across all items. `renderTagList()` builds sidebar. Tags field in modal has live badge preview. - **Extension:** `.pk-tag` badges below item subtitle. Favorites tab = items where `tags.includes('favorite')`. Favorited items show gold ★ in site label. - To favourite an item: add tag `favorite` in the web app edit modal. ### Collapsible folder groups - **Web app:** `_collapsedGroups` Set persists state across re-renders. Header shows name + count badge + chevron (▼/▶). Click toggles and re-renders. - **Extension:** only on "All items" tab. `_folders` fetched in parallel with vault. `_collapsedFolders` Set. `folderName(id)` resolves to display name with `(none)` fallback. ### Import / Export - **Encrypted JSON export:** `GET /api/vault` → versioned envelope → download. Zero-knowledge. - **CSV export:** decrypt client-side → `name, url, username, password, notes`. - **JSON import:** sends encrypted blobs to `POST /api/vault/import` directly. - **CSV import:** parses Chrome / Bitwarden / 1Password formats; encrypts client-side before POSTing. - Both endpoints write audit logs: `vault_item.export`, `vault_item.import`. ### Web-app session timeout - `_startWebIdleTracking()` called at end of `init()`. Listens to `mousemove`, `mousedown`, `keydown`, `touchstart`, `scroll`. - On timeout: `VaultSession.clear()` → unlock overlay → toast. - Stored in `localStorage` as `web_idle_minutes`. Default 15 min. Options: Never/5/10/15/30/60. - Exposed in Account Settings → Auto-Lock (`