From c054147018d1637252b0dc5f9407b1601b79e65f Mon Sep 17 00:00:00 2001 From: Nguyen Ngo Date: Thu, 7 May 2026 17:18:51 -0400 Subject: [PATCH] 05/07: updated markdown documents --- ...E.sync-conflict-20260507-154033-FEZULOS.md | 516 ++++++++++++++++++ ...E.sync-conflict-20260507-154213-FEZULOS.md | 288 ++++++++++ 2 files changed, 804 insertions(+) create mode 100644 CLAUDE.sync-conflict-20260507-154033-FEZULOS.md create mode 100644 README.sync-conflict-20260507-154213-FEZULOS.md diff --git a/CLAUDE.sync-conflict-20260507-154033-FEZULOS.md b/CLAUDE.sync-conflict-20260507-154033-FEZULOS.md new file mode 100644 index 0000000..33673cb --- /dev/null +++ b/CLAUDE.sync-conflict-20260507-154033-FEZULOS.md @@ -0,0 +1,516 @@ +# 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 (`