04/26 Update Claude.md, Readme.md
This commit is contained in:
@@ -9,14 +9,12 @@ A full-featured password manager web app and browser extension modelled after La
|
|||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
### Development (Windows)
|
### Development (Windows)
|
||||||
|
|
||||||
- **Backend:** Python 3.12, Flask 3.x
|
- **Backend:** Python 3.12, Flask 3.x
|
||||||
- **Database:** MySQL 8.x
|
- **Database:** MySQL 8.x
|
||||||
- **Frontend:** Vanilla JS (Web Crypto API) + Jinja2 templates
|
- **Frontend:** Vanilla JS (Web Crypto API) + Jinja2 templates
|
||||||
- **Dev server:** `python run.py`
|
- **Dev server:** `python run.py`
|
||||||
|
|
||||||
### Production (Ubuntu)
|
### Production (Ubuntu)
|
||||||
|
|
||||||
- **Web server:** Nginx (reverse proxy, TLS termination)
|
- **Web server:** Nginx (reverse proxy, TLS termination)
|
||||||
- **WSGI server:** Gunicorn
|
- **WSGI server:** Gunicorn
|
||||||
- **Process manager:** systemd
|
- **Process manager:** systemd
|
||||||
@@ -41,89 +39,67 @@ Web App <──> Nginx ──> Gunicorn ──> Flask App │
|
|||||||
```
|
```
|
||||||
passkeeper/
|
passkeeper/
|
||||||
├── app/
|
├── app/
|
||||||
│ ├── __init__.py # App factory (blueprints, extensions, CSRF exemptions, security headers)
|
│ ├── __init__.py # App factory, blueprints, CSRF exemptions, security headers
|
||||||
│ ├── config.py # DevelopmentConfig / ProductionConfig (TOTP_ENCRYPTION_KEY, CORS_ORIGINS, RATELIMIT_STORAGE_URI)
|
│ ├── config.py # DevelopmentConfig / ProductionConfig
|
||||||
│ ├── models/
|
│ ├── models/
|
||||||
│ │ ├── user.py # User model (Argon2id, TOTP encrypted, ECDH sharing keys, recovery columns)
|
│ │ ├── user.py # Argon2id, TOTP encrypted, ECDH keys, recovery
|
||||||
│ │ ├── vault_item.py # VaultItem model (enc_data, iv, enc_name, iv_name — all sensitive fields encrypted)
|
│ │ ├── vault_item.py # enc_data, iv, enc_name, iv_name
|
||||||
│ │ ├── folder.py # Folder model
|
│ │ ├── folder.py
|
||||||
│ │ ├── token_blacklist.py # JWT revocation on logout (jti + expires_at)
|
│ │ ├── token_blacklist.py # JWT revocation (jti + expires_at)
|
||||||
│ │ ├── shared_item.py # Cross-user ECDH-encrypted item shares
|
│ │ ├── shared_item.py # ECDH-encrypted cross-user shares
|
||||||
│ │ ├── emergency_access.py # Emergency access grants (state machine)
|
│ │ ├── emergency_access.py # State machine
|
||||||
│ │ └── audit_log.py # Server-side audit trail for all CUD actions
|
│ │ └── audit_log.py
|
||||||
│ ├── routes/
|
│ ├── routes/
|
||||||
│ │ ├── auth.py # Register, login, MFA/TOTP, logout, refresh, change-password, delete-account, recovery
|
│ │ ├── auth.py # Register, login, MFA, logout, refresh, change-password, recovery
|
||||||
│ │ ├── vault.py # CRUD vault items (accepts enc_name/iv_name on create/update)
|
│ │ ├── vault.py # CRUD + GET /export + POST /import
|
||||||
│ │ ├── folders.py # CRUD folders
|
│ │ ├── folders.py
|
||||||
│ │ ├── sharing.py # ECDH key exchange + zero-knowledge item sharing
|
│ │ ├── sharing.py
|
||||||
│ │ └── emergency.py # Emergency access state machine
|
│ │ └── emergency.py
|
||||||
│ ├── services/
|
│ ├── services/
|
||||||
│ │ └── auth_service.py # Argon2id hashing, JWT generation, blacklist, @require_jwt, TOTP encrypt/decrypt
|
│ │ └── auth_service.py # Argon2id, JWT, blacklist, @require_jwt, TOTP encrypt/decrypt
|
||||||
│ ├── static/
|
│ ├── static/
|
||||||
│ │ ├── css/app.css # Full responsive stylesheet (phone/tablet/laptop/PC breakpoints)
|
│ │ ├── css/app.css # Full responsive stylesheet; collapsible group styles; tag badge styles
|
||||||
│ │ └── js/
|
│ │ └── js/
|
||||||
│ │ ├── crypto.js # Web Crypto API: deriveAuthHash, deriveVaultKey, encryptItem, decryptItem,
|
│ │ ├── crypto.js # deriveAuthHash, deriveVaultKey, encryptItem, decryptItem,
|
||||||
│ │ │ # encryptName, decryptName, generateSalt
|
│ │ │ # encryptName, decryptName, generateSalt
|
||||||
│ │ ├── auth.js # Login/register forms + TOTP MFA step + VaultSession
|
│ │ ├── auth.js # Login/register + TOTP MFA + VaultSession
|
||||||
│ │ ├── recover.js # Account recovery flow (two-step: verify code → set new password)
|
│ │ ├── recover.js
|
||||||
│ │ ├── sharing.js # ECDH P-256 key generation, encrypt/decrypt for sharing
|
│ │ ├── sharing.js # ECDH P-256
|
||||||
│ │ └── vault.js # Vault UI: all views + sidebar toggle + change-password + recovery + delete-account
|
│ │ └── vault.js # Full vault UI — see "Key Features" below
|
||||||
│ │ # renderSecurityDashboard is async — runs HIBP k-anonymity checks after sync sections
|
│ └── templates/vault/
|
||||||
│ └── templates/
|
│ └── index.html # Tags field, Auto-Lock setting, Import/Export view + sidebar
|
||||||
│ ├── base.html # CSP meta, csrf-token meta, viewport meta, no inline scripts/styles
|
├── extension/
|
||||||
│ ├── auth/
|
│ ├── manifest.json # MV3 (Chrome/Edge); Ctrl+Shift+L keyboard shortcut
|
||||||
│ │ ├── login.html # Login + MFA step + "Forgot password?" link
|
│ ├── manifest.firefox.json # MV2 (Firefox)
|
||||||
│ │ ├── register.html
|
│ ├── background.js # Chrome SW: badges, pending-save "!" badge, CLEAR_SAVE_BADGE
|
||||||
│ │ └── recover.html # Two-step account recovery page
|
│ ├── background.firefox.js # Firefox: in-memory session shim, setTimeout idle lock
|
||||||
│ └── vault/
|
|
||||||
│ └── index.html # Sidebar (collapsible), mobile topbar, vault list, modals
|
|
||||||
├── extension/ # Browser extension (Manifest V3)
|
|
||||||
│ ├── manifest.json # MV3: permissions, content_scripts, background, action
|
|
||||||
│ ├── background.js # Service worker: badge count, save-credentials relay, SSO sync,
|
|
||||||
│ │ # KEEPALIVE ping handler, OPEN_VAULT tab opener
|
|
||||||
│ ├── shared/
|
│ ├── shared/
|
||||||
│ │ └── crypto.js # Same PBKDF2+AES-GCM logic; extractable vault key for session storage;
|
│ │ ├── crypto.js # PBKDF2+AES-GCM; encryptName/decryptName; extractable key
|
||||||
│ │ # encryptName / decryptName helpers for item name encryption
|
│ │ └── browser-polyfill.js # chrome=browser alias for Firefox content scripts
|
||||||
│ ├── popup/
|
│ ├── popup/
|
||||||
│ │ ├── popup.html # Login → MFA → Unlock-only → Vault → Generator → Add Item views
|
│ │ ├── popup.html # Tabs: All relevant / All items / Favorites / Recents
|
||||||
│ │ ├── popup.css # Includes generator styles, save-modal overlay, pk-flyout context menu
|
│ │ ├── popup.css # .pk-group-* collapsible styles; .pk-tag badge; .pk-flyout menu
|
||||||
│ │ └── popup.js # Auth, vault fetch/decrypt (decrypts enc_name), tabs, autofill trigger,
|
│ │ └── popup.js # Vault+folder fetch; collapsible groups; Favorites tab;
|
||||||
│ │ # inline password generator (fully CSPRNG via _cryptoRandInt),
|
│ │ # tag display; clipboard auto-clear; CSPRNG generator;
|
||||||
│ │ # save-prompt modal, copy-username button, three-dot flyout menu,
|
│ │ # save badge clear; three-dot flyout; copy-username button
|
||||||
│ │ # SSO, service-worker keepalive, relevant-tab domain filter
|
|
||||||
│ ├── content/
|
│ ├── content/
|
||||||
│ │ └── content.js # Form detection; _isLikelyUsernameField heuristic (autocomplete/name/id scoring);
|
│ │ └── content.js # _isLikelyUsernameField; _normaliseUrl; debounced input (150ms);
|
||||||
│ │ # injects PK icon into username AND password fields only;
|
│ │ # MutationObserver guard; vault_items_cs from chrome.storage.session
|
||||||
│ │ # debounced input handler (150ms); MutationObserver guards against
|
|
||||||
│ │ # re-triggering on extension's own DOM mutations;
|
|
||||||
│ │ # _normaliseUrl handles bare domains ("github.com")
|
|
||||||
│ ├── bridge/
|
│ ├── bridge/
|
||||||
│ │ └── bridge.js # SSO bridge (runs on vault domain only): syncs session web↔extension
|
│ │ └── bridge.js # SSO bridge (vault domain only)
|
||||||
│ ├── icons/ # Generated by make_icons.py (Pillow)
|
│ └── icons/
|
||||||
│ │ ├── icon16.png
|
├── migrations/versions/
|
||||||
│ │ ├── icon48.png
|
│ ├── 71d7158dd3b9_add_audit_log_tables_fix_sharing_public_.py
|
||||||
│ │ └── icon128.png
|
│ ├── a1b2c3d4e5f6_encrypt_totp_secret_at_rest.py
|
||||||
│ └── make_icons.py # Re-run to regenerate icons: python extension/make_icons.py
|
│ ├── b2c3d4e5f6a7_add_account_recovery_columns.py
|
||||||
├── migrations/
|
│ └── c3d4e5f6a7b8_encrypt_vault_item_name.py # adds enc_name + iv_name
|
||||||
│ ├── env.py # Alembic env (Flask-Migrate)
|
|
||||||
│ ├── script.py.mako
|
|
||||||
│ └── versions/
|
|
||||||
│ ├── 71d7158dd3b9_add_audit_log_tables_fix_sharing_public_.py
|
|
||||||
│ ├── a1b2c3d4e5f6_encrypt_totp_secret_at_rest.py # Widens totp_secret + adds totp_iv
|
|
||||||
│ ├── b2c3d4e5f6a7_add_account_recovery_columns.py # Adds recovery_enc_salt + recovery_iv
|
|
||||||
│ └── c3d4e5f6a7b8_encrypt_vault_item_name.py # Adds enc_name + iv_name to vault_items
|
|
||||||
├── scripts/
|
├── scripts/
|
||||||
│ ├── reencrypt_totp_secrets.py # One-time migration: encrypt existing plaintext TOTP secrets
|
│ ├── reencrypt_totp_secrets.py
|
||||||
│ ├── backup_db.sh # Automated MySQL backup (gzip, 30-day retention)
|
│ ├── backup_db.sh / backup.cron / passkeeper-logrotate
|
||||||
│ ├── backup.cron # Crontab entry for daily 2 AM backup
|
│ ├── passkeeper-nginx.conf / passkeeper.service
|
||||||
│ ├── passkeeper-logrotate # logrotate config for Gunicorn logs
|
├── reset_db.py
|
||||||
│ ├── passkeeper-nginx.conf # Hardened Nginx config (HSTS, rate limiting, upstream retry)
|
|
||||||
│ └── passkeeper.service # Hardened systemd unit (watchdog, preload, sandboxing)
|
|
||||||
├── reset_db.py # Drop + recreate all tables (dev only, handles FK checks)
|
|
||||||
├── requirements.txt
|
├── requirements.txt
|
||||||
├── wsgi.py # Gunicorn entry point
|
├── wsgi.py / run.py
|
||||||
├── run.py # Dev entry point
|
|
||||||
├── .env
|
|
||||||
└── CLAUDE.md
|
└── CLAUDE.md
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -136,204 +112,143 @@ passkeeper/
|
|||||||
CREATE TABLE users (
|
CREATE TABLE users (
|
||||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
email VARCHAR(255) UNIQUE NOT NULL,
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
master_hash VARCHAR(255) NOT NULL, -- Argon2id hash of client-derived PBKDF2 auth_hash
|
master_hash VARCHAR(255) NOT NULL, -- Argon2id(authHash)
|
||||||
enc_key_salt VARCHAR(64) NOT NULL, -- Random 16-byte salt (base64), returned on login
|
enc_key_salt VARCHAR(64) NOT NULL,
|
||||||
created_at DATETIME DEFAULT NOW(),
|
created_at / last_login DATETIME,
|
||||||
last_login DATETIME,
|
totp_secret VARCHAR(255), -- AES-256-GCM ciphertext
|
||||||
-- TOTP / MFA (secret AES-256-GCM encrypted at rest)
|
totp_iv VARCHAR(64),
|
||||||
totp_secret VARCHAR(255), -- AES-256-GCM ciphertext of base32 secret (base64)
|
|
||||||
totp_iv VARCHAR(64), -- base64 12-byte GCM nonce for totp_secret
|
|
||||||
totp_enabled TINYINT(1) DEFAULT 0,
|
totp_enabled TINYINT(1) DEFAULT 0,
|
||||||
-- ECDH P-256 sharing keypair
|
sharing_public_key VARCHAR(128),
|
||||||
sharing_public_key VARCHAR(128), -- Raw uncompressed point (65 bytes), base64, plaintext
|
sharing_private_key_enc TEXT,
|
||||||
sharing_private_key_enc TEXT, -- JWK, AES-256-GCM encrypted with vault key
|
sharing_private_key_iv VARCHAR(64),
|
||||||
sharing_private_key_iv VARCHAR(64), -- base64 12-byte nonce for private key encryption
|
recovery_enc_salt VARCHAR(128),
|
||||||
-- Account recovery
|
recovery_iv VARCHAR(64)
|
||||||
recovery_enc_salt VARCHAR(128), -- enc_key_salt re-encrypted with recovery key (base64)
|
|
||||||
recovery_iv VARCHAR(64) -- base64 12-byte nonce for recovery_enc_salt
|
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Folders
|
-- Vault Items
|
||||||
CREATE TABLE folders (
|
CREATE TABLE vault_items (
|
||||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
user_id INT UNSIGNED NOT NULL,
|
user_id INT UNSIGNED NOT NULL,
|
||||||
name VARCHAR(128) NOT NULL,
|
folder_id INT UNSIGNED,
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
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
|
||||||
-- Vault Items (all sensitive fields AES-256-GCM encrypted client-side)
|
iv VARCHAR(64) NOT NULL,
|
||||||
CREATE TABLE vault_items (
|
enc_name TEXT, -- AES-256-GCM ciphertext of item name (nullable)
|
||||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
iv_name VARCHAR(64),
|
||||||
user_id INT UNSIGNED NOT NULL,
|
created_at DATETIME DEFAULT NOW(),
|
||||||
folder_id INT UNSIGNED,
|
updated_at DATETIME DEFAULT NOW() ON UPDATE NOW(),
|
||||||
item_type VARCHAR(20) NOT NULL DEFAULT 'password', -- plain string, not ENUM
|
|
||||||
name VARCHAR(255) NOT NULL, -- non-sensitive server label (item type only)
|
|
||||||
enc_data TEXT NOT NULL, -- base64 AES-256-GCM ciphertext of item payload
|
|
||||||
iv VARCHAR(64) NOT NULL, -- base64 12-byte GCM nonce for enc_data
|
|
||||||
enc_name TEXT, -- base64 AES-256-GCM ciphertext of item name
|
|
||||||
iv_name VARCHAR(64), -- base64 12-byte GCM nonce for enc_name
|
|
||||||
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 (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (folder_id) REFERENCES folders(id) ON DELETE SET NULL
|
FOREIGN KEY (folder_id) REFERENCES folders(id) ON DELETE SET NULL
|
||||||
);
|
);
|
||||||
|
-- (folders, token_blacklist, shared_items, emergency_access, audit_logs — unchanged)
|
||||||
-- Token Blacklist (JWT revocation)
|
|
||||||
CREATE TABLE token_blacklist (
|
|
||||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
jti VARCHAR(36) UNIQUE NOT NULL,
|
|
||||||
user_id INT UNSIGNED NOT NULL,
|
|
||||||
expires_at DATETIME NOT NULL,
|
|
||||||
INDEX (jti), INDEX (expires_at)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Shared Items (ECDH zero-knowledge sharing)
|
|
||||||
CREATE TABLE shared_items (
|
|
||||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
item_id INT UNSIGNED NOT NULL,
|
|
||||||
owner_id INT UNSIGNED NOT NULL,
|
|
||||||
recipient_email VARCHAR(255) NOT NULL,
|
|
||||||
recipient_id INT UNSIGNED,
|
|
||||||
item_name VARCHAR(255) NOT NULL, -- plaintext display name for share inbox
|
|
||||||
item_type VARCHAR(20) NOT NULL,
|
|
||||||
enc_data TEXT NOT NULL,
|
|
||||||
iv VARCHAR(64) NOT NULL,
|
|
||||||
accepted TINYINT(1) DEFAULT 0,
|
|
||||||
created_at DATETIME DEFAULT NOW(),
|
|
||||||
FOREIGN KEY (item_id) REFERENCES vault_items(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Emergency Access
|
|
||||||
CREATE TABLE emergency_access (
|
|
||||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
grantor_id INT UNSIGNED NOT NULL,
|
|
||||||
grantee_email VARCHAR(255) NOT NULL,
|
|
||||||
grantee_id INT UNSIGNED,
|
|
||||||
wait_days INT NOT NULL DEFAULT 7,
|
|
||||||
status VARCHAR(20) NOT NULL DEFAULT 'invited',
|
|
||||||
request_initiated_at DATETIME,
|
|
||||||
enc_vault TEXT,
|
|
||||||
created_at DATETIME DEFAULT NOW(),
|
|
||||||
FOREIGN KEY (grantor_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (grantee_id) REFERENCES users(id) ON DELETE SET NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Audit Logs
|
|
||||||
CREATE TABLE audit_logs (
|
|
||||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
user_id INT UNSIGNED NOT NULL,
|
|
||||||
action VARCHAR(64) NOT NULL,
|
|
||||||
resource_type VARCHAR(64) NOT NULL,
|
|
||||||
resource_id INT UNSIGNED,
|
|
||||||
detail VARCHAR(512),
|
|
||||||
ip_address VARCHAR(45),
|
|
||||||
created_at DATETIME NOT NULL,
|
|
||||||
INDEX (user_id), INDEX (created_at)
|
|
||||||
);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 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
|
## Security Model
|
||||||
|
|
||||||
- **Zero-knowledge:** master password never sent to server
|
- **Zero-knowledge:** master password never sent to server
|
||||||
- `authHash = PBKDF2(masterPassword, email, 100_000 iter)` — sent to server for auth only
|
- `authHash = PBKDF2(masterPassword, email, 100k iter)` → auth only
|
||||||
- `vaultKey = PBKDF2(masterPassword, enc_key_salt, 600_000 iter)` — stays in browser memory only
|
- `vaultKey = PBKDF2(masterPassword, enc_key_salt, 600k iter)` → browser memory only
|
||||||
- All vault data (payload + item name) encrypted client-side (AES-256-GCM) before sending to server
|
- All vault data (payload + name + tags) encrypted client-side (AES-256-GCM)
|
||||||
- **Item name encryption:** `enc_name`/`iv_name` store the AES-256-GCM ciphertext of the item name. The server's plaintext `name` column holds only the item type (e.g. `"password"`) as a non-sensitive audit label. Legacy items without `enc_name` fall back to `name` transparently.
|
- **Item name:** `enc_name`/`iv_name`; server `name` column = item type only
|
||||||
- **Server-side:** Argon2id hash of the client-derived `authHash` (double-hashed for depth)
|
- **Tags:** `plain.tags: string[]` inside `enc_data`; server never sees them
|
||||||
- **JWT:** HS256, access token 15 min, refresh token 7 days, unique JTI per token
|
- **Argon2id:** double-hashes `authHash` server-side
|
||||||
- **JWT blacklist:** on logout both tokens are blacklisted by JTI; refresh rotates tokens
|
- **JWT:** HS256, 15 min access / 7 day refresh, JTI blacklisted on logout
|
||||||
- **MFA:** TOTP (pyotp), secret stored AES-256-GCM encrypted in `users.totp_secret`/`totp_iv`
|
- **MFA:** TOTP secret AES-256-GCM encrypted at rest
|
||||||
- **Sharing:** ECDH P-256 — shared secret derived client-side, re-encrypts item plaintext
|
- **Sharing:** ECDH P-256 zero-knowledge re-encryption
|
||||||
- **Emergency access:** same ECDH re-encryption; wait timer enforced server-side
|
- **Password generator:** fully CSPRNG (`_cryptoRandInt` rejection-sampling)
|
||||||
- **CSRF:** Flask-WTF on page-serving routes; API blueprints CSRF-exempt (JWT bearer auth)
|
- **Decrypted vault data:** `chrome.storage.session` only — never to disk
|
||||||
- **Rate limiting:** Flask-Limiter (Redis-backed in production); Nginx dual-zone as second layer
|
- **Clipboard auto-clear:** 30 s after any password/username copy (web + extension)
|
||||||
- **HTTP security headers:** HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, CSP via `@app.after_request`
|
- **Breach detection:** HIBP k-anonymity — only 5-char SHA-1 prefix transmitted
|
||||||
- **Password generator (extension):** Fully CSPRNG — `_cryptoRandInt()` uses rejection-sampling with `crypto.getRandomValues()`. `Math.random()` is never called anywhere in the generator.
|
|
||||||
- **Decrypted vault data in extension:** Stored in `chrome.storage.session` only (memory-only, cleared on browser close). Never written to `chrome.storage.local`.
|
|
||||||
- **Breach detection:** Security dashboard runs HaveIBeenPwned k-anonymity checks — only the first 5 chars of each password's SHA-1 hash are transmitted. Passwords never leave the browser.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Extension Storage Architecture — Critical Rules
|
## Extension Storage Architecture
|
||||||
|
|
||||||
### Storage area decision table
|
| Data | Storage | Reason |
|
||||||
|
|
||||||
| Data | Where stored | Why |
|
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `access_token`, `vault_key_jwk`, `vault_items`, `vault_items_cs` | `chrome.storage.session` | Memory-only, cleared on browser close. Decrypted data must never persist to disk. |
|
| `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 browser restarts and SW termination. |
|
| `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 |
|
||||||
|
|
||||||
### `vault_items_cs` is in `chrome.storage.session`, NOT `chrome.storage.local`
|
### 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.
|
||||||
|
|
||||||
As of the current codebase, `vault_items_cs` (the decrypted vault cache for content scripts) is written to **`chrome.storage.session`**. This is a deliberate security decision — decrypted passwords must never be persisted 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.
|
||||||
**Minimum Chrome version required: 111 (March 2023).** `chrome.storage.session` is not readable by content scripts on Chrome ≤ 110. The `storage.onChanged` listener in `content.js` watches `area === 'session'`.
|
|
||||||
|
|
||||||
> ⚠️ **The original CLAUDE.md stated `vault_items_cs` lives in `chrome.storage.local`. That is no longer correct.** If you see it reverting to `local`, that is a regression — revert it.
|
|
||||||
|
|
||||||
### `chrome.storage.onChanged` area guard
|
|
||||||
|
|
||||||
```js
|
|
||||||
chrome.storage.onChanged.addListener(function(changes, area) {
|
|
||||||
if (area === 'session' && changes.vault_items_cs) { // 'session', not 'local'
|
|
||||||
...
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### `pending_save` must remain in `chrome.storage.local`
|
|
||||||
|
|
||||||
`chrome.storage.session` is wiped when the MV3 service worker is killed (within seconds of inactivity). `pending_save` stays in `local` so the save prompt survives SW restarts.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Key Implementation Decisions & Gotchas
|
## Key Implementation Details
|
||||||
|
|
||||||
### Item name is encrypted client-side (`enc_name` / `iv_name`)
|
### 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.
|
||||||
|
|
||||||
The `name` column in `vault_items` is **not** sensitive — it stores only the item type string (e.g. `"password"`) as a server-side audit label. The real display name is stored as AES-256-GCM ciphertext in `enc_name` with nonce `iv_name`.
|
### 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.
|
||||||
|
|
||||||
**On load:** `loadVault()` / `fetchAndDecryptVault()` decrypt `enc_name` after decrypting `enc_data` and override `item.name` with the plaintext. Legacy items without `enc_name` use the server's `name` field as fallback — no migration of existing rows required.
|
### 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`.
|
||||||
|
|
||||||
**On save:** `handleFormSubmit()` (web app) and `addItemToVault()` / `saveCredential()` (extension) call `Crypto.encryptName(vaultKey, name)` / `ExtCrypto.encryptName(vaultKey, name)` before POSTing, sending `enc_name` + `iv_name` alongside `enc_data` + `iv`.
|
### 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 (`<select id="web-idle-select">`).
|
||||||
|
|
||||||
**On password change / recovery:** The re-encryption loops in `auth.py` also update `vault_item.enc_name` / `vault_item.iv_name` when the client sends updated values.
|
### Browser history / back-button
|
||||||
|
- `switchView(view, { pushState = true })` calls `history.pushState({ view }, '', '#view-name')`.
|
||||||
|
- `popstate` listener in `init()` restores view. Direct hash links (`/vault#security`) work.
|
||||||
|
- `Vault.switchToImportExport()` exposed on public return object for sidebar `<li onclick>` fallback.
|
||||||
|
|
||||||
**API contract:** `POST /api/vault` and `PUT /api/vault/<id>` now accept optional `enc_name` and `iv_name` fields. The `name` field is still required (used as the server-side label).
|
### Clipboard auto-clear
|
||||||
|
- Web app: `copyToClipboard()` uses `_clipboardClearTimer` setTimeout 30s → `writeText('')`.
|
||||||
|
- Extension: `_copyWithAutoClear()` same pattern. Applied to password, username, and flyout copies.
|
||||||
|
|
||||||
### Password generator is fully CSPRNG
|
### Missing 2FA warning
|
||||||
|
- Security dashboard "No 2FA Saved" section: password items with URL but no `totp_uri`.
|
||||||
|
- Uses existing `extractTotpSecret()` and `makeSection()`.
|
||||||
|
|
||||||
`generatePassword()` in `extension/popup/popup.js` uses `_cryptoRandInt(max)` for all random selection:
|
### Security score age penalty fix
|
||||||
|
- `old` only includes items that are **also weak or reused** (`weakOrReusedIds.has(i.id)`).
|
||||||
|
- Strong unique unchanged passwords no longer penalised.
|
||||||
|
|
||||||
```js
|
### Keyboard shortcut
|
||||||
function _cryptoRandInt(max) {
|
- `manifest.json`: `commands._execute_action`, `Ctrl+Shift+L` / `Cmd+Shift+L`.
|
||||||
// Rejection sampling — no modulo bias.
|
- `manifest.firefox.json`: `_execute_browser_action`.
|
||||||
const limit = Math.floor(0x100000000 / max) * max;
|
- Customisable at `chrome://extensions/shortcuts`.
|
||||||
const buf = new Uint32Array(1);
|
|
||||||
do { crypto.getRandomValues(buf); } while (buf[0] >= limit);
|
|
||||||
return buf[0] % max;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This replaces `Math.floor(Math.random() * n)` which was used in the required-character selection and Fisher-Yates shuffle steps. `Math.random()` is not called anywhere in the generator.
|
### Firefox compatibility
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `manifest.firefox.json` | MV2: `browser_action`, `background.scripts` |
|
||||||
|
| `background.firefox.js` | In-memory session shim, `setTimeout` idle lock, `browserAction` API |
|
||||||
|
| `shared/browser-polyfill.js` | `chrome = browser` alias for content scripts |
|
||||||
|
|
||||||
### `_isLikelyUsernameField()` — credential field detection heuristic
|
Load in Firefox: `about:debugging` → This Firefox → Load Temporary Add-on → `manifest.firefox.json`.
|
||||||
|
|
||||||
`findUsernameField()` in `content.js` no longer accepts any `input[type="text"]` preceding the password field. It scores candidates via `_isLikelyUsernameField()`:
|
|
||||||
|
|
||||||
1. **Definite YES:** `autocomplete="username"` or `"email"` or `"tel"`
|
|
||||||
2. **Definite NO:** Non-credential `autocomplete` values (`name`, `given-name`, `organization`, `search`, etc.)
|
|
||||||
3. **Keyword scan YES:** `name`, `id`, `placeholder`, or `aria-label` contain `user|email|mail|login|phone|tel|mobile|account`
|
|
||||||
4. **Otherwise:** field is not decorated
|
|
||||||
|
|
||||||
`input[type="email"]` and `input[type="tel"]` are always accepted without scoring.
|
|
||||||
|
|
||||||
### `_normaliseUrl()` — bare-domain URL matching
|
|
||||||
|
|
||||||
Both `content.js` and `popup.js` include a `_normaliseUrl()` helper that prepends `https://` to URLs that lack a scheme before passing them to `new URL()`. This prevents silent match failures for vault items stored with bare domains (e.g. `"github.com"` instead of `"https://github.com"`).
|
|
||||||
|
|
||||||
|
### `_normaliseUrl()` — bare-domain matching
|
||||||
```js
|
```js
|
||||||
function _normaliseUrl(raw) {
|
function _normaliseUrl(raw) {
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
@@ -343,277 +258,138 @@ function _normaliseUrl(raw) {
|
|||||||
return 'https://' + s;
|
return 'https://' + s;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
In both `content.js` and `popup.js`. Prevents silent match failures for bare domains.
|
||||||
|
|
||||||
### MutationObserver guards against extension's own DOM mutations
|
### `_isLikelyUsernameField()` — credential field heuristic
|
||||||
|
1. **YES:** `autocomplete="username|email|tel"`
|
||||||
|
2. **NO:** non-credential autocomplete (`name`, `organization`, `search`, etc.)
|
||||||
|
3. **YES:** `name/id/placeholder/aria-label` matches `user|email|mail|login|phone|tel|mobile|account`
|
||||||
|
4. **Otherwise:** not decorated
|
||||||
|
|
||||||
The `MutationObserver` in `content.js` inspects every mutation before calling `decorateFields()`. If every added/removed node carries a `__pk` class or id prefix, the callback returns early — preventing re-decoration loops on SPAs triggered by the extension injecting or removing its own dropdown or icon buttons.
|
### MutationObserver guard
|
||||||
|
Inspects added/removed nodes — if all carry `__pk` prefix, returns early. Prevents re-decoration loops when the extension injects/removes its own UI.
|
||||||
|
|
||||||
### `showDropdown` is debounced on `input` events
|
### `showDropdown` debounced 150ms on `input`
|
||||||
|
`_debounce(fn, ms)` helper. `focus` listener remains instant.
|
||||||
|
|
||||||
The `input` event listener on decorated fields wraps `showDropdown` with a 150 ms debounce via `_debounce()`. The `focus` listener remains instant. This eliminates visible lag on large vaults when the user types quickly.
|
### Three-dot flyout menu
|
||||||
|
`.pk-flyout` div anchored below button. Dynamic items: Open URL / Copy username / Copy password. Closes on outside click.
|
||||||
### Three-dot menu is now a proper flyout
|
|
||||||
|
|
||||||
The `data-menu` button in the popup renders a `.pk-flyout` div anchored below the button. Menu items are built dynamically from what the item has — "Open URL" only if `plain.url` exists, etc. Styled via `.pk-flyout` / `.pk-flyout-item` in `popup.css`. Closes on any outside click.
|
|
||||||
|
|
||||||
### Copy-username button is a dedicated action
|
|
||||||
|
|
||||||
A `data-copy-user` button with a person SVG icon appears at the left of the action row for any item with a username. This replaces the previous behaviour where the three-dot button silently copied the username on click.
|
|
||||||
|
|
||||||
### HaveIBeenPwned breach detection
|
|
||||||
|
|
||||||
`renderSecurityDashboard()` in `vault.js` is now `async`. After rendering the synchronous sections (Weak / Reused / Old), it fires parallel HIBP API calls for every password item via `checkHibp(password)`:
|
|
||||||
|
|
||||||
```js
|
|
||||||
async function checkHibp(password) {
|
|
||||||
const hashHex = /* SHA-1 of password via crypto.subtle */;
|
|
||||||
const prefix = hashHex.slice(0, 5);
|
|
||||||
const suffix = hashHex.slice(5);
|
|
||||||
const res = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`, {
|
|
||||||
headers: { 'Add-Padding': 'true' },
|
|
||||||
});
|
|
||||||
// parse response, find matching suffix, return breach count
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Only the first 5 hex characters of the SHA-1 hash are sent. Passwords never leave the browser. Network errors fail safe (return 0, do not block UI). The HIBP section renders progressively after the synchronous sections.
|
|
||||||
|
|
||||||
### `item_type` must be `VARCHAR(20)`, NOT `db.Enum(ItemType)`
|
|
||||||
|
|
||||||
Using `db.Enum(ItemType)` with a `str`-based Python enum caused SQLAlchemy to lazy-load the column back as a raw string after commit, making `self.item_type.value` raise `AttributeError`. Use `db.String(20)`.
|
|
||||||
|
|
||||||
### `INTEGER(unsigned=True)` requires MySQL dialect type
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sqlalchemy.dialects.mysql import INTEGER
|
|
||||||
db.Column(INTEGER(unsigned=True), ...)
|
|
||||||
```
|
|
||||||
|
|
||||||
### All datetimes are naive UTC (`datetime.utcnow()`)
|
|
||||||
|
|
||||||
Do NOT mix in `datetime.now(timezone.utc)` — mixing causes comparison failures in token blacklist and emergency access wait-timer checks.
|
|
||||||
|
|
||||||
### AuditLog must flush before log, then commit together
|
|
||||||
|
|
||||||
|
### AuditLog pattern
|
||||||
```python
|
```python
|
||||||
db.session.add(item)
|
db.session.add(item)
|
||||||
db.session.flush() # populates item.id
|
db.session.flush() # populates item.id
|
||||||
AuditLog.log(user_id=..., action='vault_item.create', resource_id=item.id, ...)
|
AuditLog.log(user_id=..., action='vault_item.create', resource_id=item.id, ...)
|
||||||
db.session.commit() # commits both atomically
|
db.session.commit() # atomic
|
||||||
```
|
```
|
||||||
|
For deletes: capture `id` and `name` before flush.
|
||||||
|
|
||||||
For deletes, capture `id` and `name` before the delete flush.
|
### Common gotchas
|
||||||
|
- `item_type`: use `db.String(20)`, not `db.Enum(ItemType)` — enum lazy-load breaks `.value`
|
||||||
### Extension vault key must be extractable
|
- `INTEGER(unsigned=True)`: requires `from sqlalchemy.dialects.mysql import INTEGER`
|
||||||
|
- All datetimes: naive UTC `datetime.utcnow()` — never mix with timezone-aware
|
||||||
`extension/shared/crypto.js` uses `extractable: true` so the vault key can be serialised as JWK and stored in `chrome.storage.session`. The web app uses `extractable: false`.
|
- Extension vault key: `extractable: true` (web app uses `false`)
|
||||||
|
- PyJWT `sub`: `str(user_id)` on encode, `int(payload['sub'])` on decode
|
||||||
### JWT `sub` claim must be a string (PyJWT 2.x)
|
- TOTP key generation: `python -c "import secrets; print(secrets.token_hex(32))"`
|
||||||
|
- `#vault-list` ID must not be renamed — `vault.js` renders into it directly
|
||||||
Always convert: `'sub': str(user_id)` when generating tokens; `int(payload['sub'])` when reading back.
|
|
||||||
|
|
||||||
### TOTP secrets encrypted at rest (server-side)
|
|
||||||
|
|
||||||
`totp_secret` stores AES-256-GCM ciphertext; `totp_iv` stores the nonce. Encrypted/decrypted exclusively by `auth_service.encrypt_totp_secret()` / `auth_service.decrypt_totp_secret()`. Generating the key:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python -c "import secrets; print(secrets.token_hex(32))"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Extension SSO bridge
|
|
||||||
|
|
||||||
`bridge.js` runs on `pwkeeper.ngodanguyen.tech` only and syncs sessions bidirectionally between the web app and the extension. See the original CLAUDE.md for the full bridge flow.
|
|
||||||
|
|
||||||
### Autofill uses native HTMLInputElement setter for framework compatibility
|
|
||||||
|
|
||||||
```js
|
|
||||||
const nativeSet = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
|
|
||||||
if (nativeSet) nativeSet.call(el, value);
|
|
||||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
||||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
||||||
```
|
|
||||||
|
|
||||||
### `reset_db.py` for schema changes (Windows, no flask db CLI)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python reset_db.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Production schema changes: `flask db upgrade`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Audit Log System
|
## Audit Action Catalog
|
||||||
|
|
||||||
Every create, edit, and delete action is recorded server-side via `AuditLog.log()`. No sensitive or encrypted values are ever written to the audit log.
|
| Module | Action | Trigger |
|
||||||
|
|---|---|---|
|
||||||
### Audit Action Catalog
|
| `auth.py` | `auth.register` | New account |
|
||||||
|
| `auth.py` | `auth.login` / `auth.login_failed` | Login success/fail |
|
||||||
| Route module | Action string | Trigger |
|
| `auth.py` | `auth.mfa_enable/disable/verify` | TOTP actions |
|
||||||
| -------------- | ---------------------------------- | ------------------------------------------------- |
|
| `auth.py` | `auth.change_password` / `auth.change_password_failed` | Password change |
|
||||||
| `auth.py` | `auth.register` | New account created |
|
| `auth.py` | `auth.delete_account` / `auth.delete_account_failed` | Deletion |
|
||||||
| `auth.py` | `auth.login` | Successful password verification |
|
| `auth.py` | `auth.recovery_setup/failed/items_denied/success` | Recovery |
|
||||||
| `auth.py` | `auth.login_failed` | Failed password attempt (known email) |
|
| `vault.py` | `vault_item.create/update/delete` | CRUD |
|
||||||
| `auth.py` | `auth.mfa_enable` | TOTP enabled after verification |
|
| `vault.py` | `vault_item.export` / `vault_item.import` | Import/Export |
|
||||||
| `auth.py` | `auth.mfa_disable` | TOTP disabled after verification |
|
| `folders.py` | `folder.create/update/delete` | Folder CRUD |
|
||||||
| `auth.py` | `auth.mfa_verify` | MFA step completed, session tokens issued |
|
| `sharing.py` | `sharing_keys.create/update` | ECDH key setup |
|
||||||
| `auth.py` | `auth.change_password` | Master password changed, vault re-encrypted |
|
| `sharing.py` | `shared_item.create/delete/accept` | Sharing |
|
||||||
| `auth.py` | `auth.change_password_failed` | Password change rejected — wrong current password |
|
| `emergency.py` | `emergency_access.*` | All EA state transitions |
|
||||||
| `auth.py` | `auth.delete_account` | Account permanently deleted |
|
|
||||||
| `auth.py` | `auth.delete_account_failed` | Deletion rejected — wrong password |
|
|
||||||
| `auth.py` | `auth.recovery_setup` | Recovery code configured |
|
|
||||||
| `auth.py` | `auth.recovery_failed` | Recovery attempt — incorrect code |
|
|
||||||
| `auth.py` | `auth.recovery_items_denied` | Recovery items fetch — incorrect proof |
|
|
||||||
| `auth.py` | `auth.recovery_success` | Account recovered, vault re-encrypted |
|
|
||||||
| `vault.py` | `vault_item.create` | Vault item created |
|
|
||||||
| `vault.py` | `vault_item.update` | Vault item updated |
|
|
||||||
| `vault.py` | `vault_item.delete` | Vault item deleted |
|
|
||||||
| `folders.py` | `folder.create` | Folder created |
|
|
||||||
| `folders.py` | `folder.update` | Folder renamed |
|
|
||||||
| `folders.py` | `folder.delete` | Folder deleted |
|
|
||||||
| `sharing.py` | `sharing_keys.create` | ECDH keypair stored for first time |
|
|
||||||
| `sharing.py` | `sharing_keys.update` | ECDH keypair replaced |
|
|
||||||
| `sharing.py` | `shared_item.create` | Item shared with another user |
|
|
||||||
| `sharing.py` | `shared_item.delete` | Share revoked by owner |
|
|
||||||
| `sharing.py` | `shared_item.accept` | Recipient accepted a share |
|
|
||||||
| `emergency.py` | `emergency_access.create` | Emergency access invitation sent |
|
|
||||||
| `emergency.py` | `emergency_access.delete` | Emergency access grant removed |
|
|
||||||
| `emergency.py` | `emergency_access.accept` | Grantee accepted invitation |
|
|
||||||
| `emergency.py` | `emergency_access.provide_vault` | Grantor uploaded encrypted vault snapshot |
|
|
||||||
| `emergency.py` | `emergency_access.request` | Grantee initiated access request |
|
|
||||||
| `emergency.py` | `emergency_access.deny` | Grantor denied pending request |
|
|
||||||
| `emergency.py` | `emergency_access.vault_retrieved` | Grantee fetched vault after wait elapsed |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## REST API Endpoints
|
## REST API
|
||||||
|
|
||||||
All `/api/vault/*` and `/api/folders/*` routes require `Authorization: Bearer <access_token>`.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
POST /api/auth/register
|
POST /api/auth/register|login|logout|refresh|recover
|
||||||
POST /api/auth/login
|
GET /api/auth/me|mfa/status|mfa/setup|recovery/status|recovery/data|recovery/items
|
||||||
POST /api/auth/logout
|
POST /api/auth/mfa/enable|disable|verify|change-password|recovery/setup
|
||||||
POST /api/auth/refresh
|
|
||||||
GET /api/auth/me
|
|
||||||
GET /api/auth/mfa/status
|
|
||||||
GET /api/auth/mfa/setup
|
|
||||||
POST /api/auth/mfa/enable
|
|
||||||
POST /api/auth/mfa/disable
|
|
||||||
POST /api/auth/mfa/verify
|
|
||||||
POST /api/auth/change-password # { current_auth_hash, new_auth_hash, new_enc_key_salt,
|
|
||||||
# items: [{id, enc_data, iv, enc_name?, iv_name?}] }
|
|
||||||
DELETE /api/auth/account
|
DELETE /api/auth/account
|
||||||
POST /api/auth/recovery/setup
|
|
||||||
GET /api/auth/recovery/status
|
|
||||||
GET /api/auth/recovery/data
|
|
||||||
GET /api/auth/recovery/items
|
|
||||||
POST /api/auth/recover
|
|
||||||
|
|
||||||
GET /api/vault
|
GET /api/vault # list all encrypted items
|
||||||
POST /api/vault # { name, item_type, folder_id, enc_data, iv, enc_name?, iv_name? }
|
POST /api/vault # { name, item_type, folder_id, enc_data, iv, enc_name?, iv_name? }
|
||||||
GET /api/vault/<id>
|
GET /api/vault/<id>
|
||||||
PUT /api/vault/<id> # accepts enc_name, iv_name
|
PUT /api/vault/<id>
|
||||||
DELETE /api/vault/<id>
|
DELETE /api/vault/<id>
|
||||||
|
GET /api/vault/export # returns encrypted item array
|
||||||
|
POST /api/vault/import # accepts array; returns { imported, skipped }
|
||||||
|
|
||||||
GET /api/folders
|
GET|POST /api/folders
|
||||||
POST /api/folders
|
PUT|DELETE /api/folders/<id>
|
||||||
PUT /api/folders/<id>
|
|
||||||
DELETE /api/folders/<id>
|
|
||||||
|
|
||||||
GET /api/sharing/keys
|
GET|POST /api/sharing/keys
|
||||||
POST /api/sharing/keys
|
GET /api/sharing/public-key
|
||||||
GET /api/sharing/public-key
|
GET|POST /api/sharing
|
||||||
GET /api/sharing
|
DELETE /api/sharing/<id>
|
||||||
POST /api/sharing
|
GET /api/sharing/inbox
|
||||||
DELETE /api/sharing/<id>
|
POST /api/sharing/inbox/<id>/accept
|
||||||
GET /api/sharing/inbox
|
|
||||||
POST /api/sharing/inbox/<id>/accept
|
|
||||||
|
|
||||||
GET /api/emergency
|
GET|POST /api/emergency
|
||||||
POST /api/emergency
|
DELETE /api/emergency/<id>
|
||||||
DELETE /api/emergency/<id>
|
POST /api/emergency/<id>/accept|provide|request|deny
|
||||||
POST /api/emergency/<id>/accept
|
GET /api/emergency/<id>/vault
|
||||||
POST /api/emergency/<id>/provide
|
|
||||||
POST /api/emergency/<id>/request
|
|
||||||
POST /api/emergency/<id>/deny
|
|
||||||
GET /api/emergency/<id>/vault
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Development Setup (Windows)
|
## Development Setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m venv .venv
|
python -m venv .venv && .venv\Scripts\activate
|
||||||
.venv\Scripts\activate
|
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
# Edit .env: MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DB, SECRET_KEY, JWT_SECRET_KEY, TOTP_ENCRYPTION_KEY
|
# .env: MYSQL_*, SECRET_KEY, JWT_SECRET_KEY, TOTP_ENCRYPTION_KEY
|
||||||
mysql -u root -p -e "CREATE DATABASE passkeeper CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
mysql -u root -p -e "CREATE DATABASE passkeeper CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
||||||
python reset_db.py
|
python reset_db.py
|
||||||
python run.py
|
python run.py
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## Production Deployment
|
||||||
|
|
||||||
## Production Deployment (Ubuntu + Nginx + Gunicorn + systemd)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Schema migrations (all subsequent deployments)
|
|
||||||
flask db upgrade
|
flask db upgrade
|
||||||
|
|
||||||
# Reload app
|
|
||||||
sudo systemctl reload passkeeper
|
sudo systemctl reload passkeeper
|
||||||
```
|
```
|
||||||
|
|
||||||
For full initial setup instructions (Nginx config, systemd unit, Certbot, etc.) refer to the original deployment guide in this file's history or `scripts/passkeeper-nginx.conf` / `scripts/passkeeper.service`.
|
## Python Dependencies
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Python Dependencies (`requirements.txt`)
|
|
||||||
|
|
||||||
```
|
```
|
||||||
flask>=3.0
|
flask>=3.0 flask-sqlalchemy>=3.1 flask-migrate>=4.0 flask-login>=0.6
|
||||||
flask-sqlalchemy>=3.1
|
flask-wtf>=1.2 flask-limiter>=3.5 flask-cors>=4.0
|
||||||
flask-migrate>=4.0
|
pymysql>=1.1 argon2-cffi>=23.1 pyjwt>=2.8 python-dotenv>=1.0
|
||||||
flask-login>=0.6
|
gunicorn>=21.0 pyotp>=2.9.0 qrcode[pil]>=7.4.2
|
||||||
flask-wtf>=1.2
|
cryptography>=42.0 # TOTP secret encryption
|
||||||
flask-limiter>=3.5
|
redis>=5.0 # Rate-limit storage (required in production)
|
||||||
flask-cors>=4.0
|
|
||||||
pymysql>=1.1
|
|
||||||
argon2-cffi>=23.1
|
|
||||||
pyjwt>=2.8
|
|
||||||
python-dotenv>=1.0
|
|
||||||
gunicorn>=21.0
|
|
||||||
pyotp>=2.9.0
|
|
||||||
qrcode[pil]>=7.4.2
|
|
||||||
cryptography>=42.0 # AES-256-GCM server-side TOTP secret encryption
|
|
||||||
redis>=5.0 # Shared rate-limit storage across Gunicorn workers
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Migration History
|
|
||||||
|
|
||||||
| Revision | Description |
|
|
||||||
|------------------|----------------------------------------------------------|
|
|
||||||
| `71d7158dd3b9` | Add audit_logs table; fix sharing_public_key column type |
|
|
||||||
| `a1b2c3d4e5f6` | Encrypt TOTP secret at rest (totp_secret → VARCHAR(255), add totp_iv) |
|
|
||||||
| `b2c3d4e5f6a7` | Add account recovery columns (recovery_enc_salt, recovery_iv) |
|
|
||||||
| `c3d4e5f6a7b8` | Encrypt vault item name (add enc_name, iv_name to vault_items) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Never log decrypted vault data server-side
|
- Never log decrypted vault data server-side
|
||||||
- All encryption/decryption of vault data (including item names) happens in the browser
|
- Tags live in `enc_data` as `plain.tags: string[]` — no schema change ever needed
|
||||||
- The server stores only encrypted blobs — zero-knowledge architecture
|
- `vault_items_cs` is in `chrome.storage.session` — decrypted data never written to disk
|
||||||
- `enc_key_salt` is not secret on its own — only useful combined with the master password
|
- HIBP checks run progressively — synchronous sections render first, then parallel async checks
|
||||||
- **Redis is required in production** for rate limiting. Without it, each Gunicorn worker maintains its own counter.
|
- Clipboard cleared 30 s after every password/username copy (web app + extension)
|
||||||
- **Recovery code is never stored server-side** — only the AES-256-GCM ciphertext of `enc_key_salt`
|
- Web-app idle timeout in `localStorage` (`web_idle_minutes`); default 15 min
|
||||||
- **Password change clears the recovery code** — user must regenerate from Account Settings
|
- Browser back/forward works for all five vault views via `history.pushState`
|
||||||
- **The `#vault-list` element ID must not be renamed** — `vault.js` renders all vault items into it
|
- Pending save badge (`"!"`) set on `SAVE_CREDENTIALS`, cleared via `CLEAR_SAVE_BADGE`
|
||||||
- **`save_blocklist`** in `chrome.storage.local` stores hostnames for which the save banner is suppressed
|
- Firefox: use `manifest.firefox.json` + `background.firefox.js` + `browser-polyfill.js`
|
||||||
- **`vault_items_cs` is in `chrome.storage.session`** — decrypted data never written to disk (changed from `local`)
|
- Redis required in production for rate limiting
|
||||||
- **HIBP checks run progressively** — the security dashboard renders synchronous sections first, then fires parallel k-anonymity requests in the background
|
- Recovery code never stored server-side; password change clears it (user must regenerate)
|
||||||
|
- `save_blocklist` in `chrome.storage.local` suppresses save banner per hostname
|
||||||
@@ -1,38 +1,50 @@
|
|||||||
# PassKeeper 🔐
|
# PassKeeper 🔐
|
||||||
|
|
||||||
A self-hosted, zero-knowledge password manager — web app and browser extension. Modelled after LastPass, built on Flask + MySQL + Web Crypto API.
|
A self-hosted, zero-knowledge password manager — web app and Chrome/Firefox extension. Your master password and decrypted vault data **never leave your browser**.
|
||||||
|
|
||||||
Your master password and decrypted vault data **never leave your browser**. The server stores only encrypted blobs it cannot read.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### Web App
|
### Web App
|
||||||
- **Zero-knowledge encryption** — AES-256-GCM client-side, 600k-iteration PBKDF2 vault key derivation
|
- **Zero-knowledge encryption** — AES-256-GCM client-side; 600k-iteration PBKDF2 vault key
|
||||||
- **7 item types** — Passwords, Secure Notes, Payment Cards, Bank Accounts, Addresses, Identities, Passkeys
|
- **7 item types** — Passwords, Secure Notes, 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
|
- **Vault item tags** — comma-separated tags stored inside the encrypted blob; sidebar tag filter; live badge preview in modal; no schema change required
|
||||||
- **Folder organisation** — create, rename, delete folders; filter vault by folder
|
- **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
|
||||||
- **Item sharing** — zero-knowledge ECDH P-256 re-encryption; share with any registered user
|
- **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; share with any registered user
|
||||||
- **Emergency access** — configurable wait-timer access grant for a trusted contact
|
- **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)
|
- **Security dashboard** — weak / reused / old / **no 2FA saved** / **HaveIBeenPwned breach check** (k-anonymity — passwords never transmitted)
|
||||||
- **Account MFA** — TOTP-based login verification (Google Authenticator / Authy)
|
- **Import / Export** — encrypted JSON backup; CSV export (plaintext, handle carefully); import from Chrome, Bitwarden, and 1Password CSV formats
|
||||||
- **Master password change** — atomic zero-knowledge re-encryption of the entire vault
|
- **Account MFA** — TOTP-based login (Google Authenticator / Authy)
|
||||||
|
- **Master password change** — atomic zero-knowledge re-encryption of entire vault including item names
|
||||||
- **Account recovery** — 128-bit recovery code; server never stores it
|
- **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
|
- **Audit log** — server-side trail of all create/edit/delete/import/export actions
|
||||||
- **Encrypted item names** — item names stored as AES-256-GCM ciphertext; server holds only the item type as a label
|
- **Encrypted item names** — `enc_name`/`iv_name`; server holds only the item type as a label
|
||||||
- **Responsive layout** — phone, tablet, laptop, and large desktop breakpoints; collapsible sidebar
|
- **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)
|
### 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 text inputs)
|
- **Autofill** — detects login forms; injects icon into username and password fields only (scored heuristic, not all inputs)
|
||||||
- **Suggestion dropdown** — anchored below the focused field; filters as you type; keyboard navigation (↑↓ Enter); fills with one click
|
- **Smart domain matching** — matches by domain name; handles bare domains (`github.com`) and subdomains
|
||||||
- **Smart domain matching** — matches vault items by domain name, handles bare domains (`github.com`) and subdomains
|
- **Suggestion dropdown** — filters as you type; keyboard navigation (↑↓ Enter); one-click fill
|
||||||
- **Auto-save banner** — prompts to save or update credentials on form submit; duplicate detection; "Never for this site" blocklist
|
- **Collapsible folder groups** — "All items" tab groups by folder with collapse/expand toggle matching the web app UX
|
||||||
- **Inline password generator** — length slider, charset toggles, strength indicator; fully CSPRNG (`crypto.getRandomValues` throughout)
|
- **Favorites tab** — items tagged `favorite` in the web app appear in a dedicated tab; marked with ★
|
||||||
- **TOTP live display** — 6-digit code + countdown timer per item in the popup
|
- **Tag display** — purple badge pills on item rows; tags sourced from `plain.tags[]`
|
||||||
- **SSO bridge** — log in once on the web app; extension picks up the session automatically
|
- **Three-dot flyout menu** — contextual actions: Open URL / Copy username / Copy password; built dynamically from what the item has
|
||||||
- **Idle lock** — configurable auto-lock timeout (1 / 5 / 10 / 30 min / Never)
|
- **Dedicated copy-username button** — person icon alongside copy-password in every item row
|
||||||
- **Security** — decrypted vault data stored in `chrome.storage.session` only (memory-only, never written to disk)
|
- **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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -42,18 +54,19 @@ Your master password and decrypted vault data **never leave your browser**. The
|
|||||||
Master Password
|
Master Password
|
||||||
│
|
│
|
||||||
├─ PBKDF2(email, 100k iter) ──► authHash ──► POST /api/auth/login
|
├─ PBKDF2(email, 100k iter) ──► authHash ──► POST /api/auth/login
|
||||||
│ │
|
│ Argon2id(authHash) stored in DB
|
||||||
│ Argon2id(authHash) stored in DB
|
|
||||||
│
|
│
|
||||||
└─ PBKDF2(enc_key_salt, 600k iter) ──► vaultKey (stays in browser memory only)
|
└─ PBKDF2(enc_key_salt, 600k iter) ──► vaultKey (browser memory only)
|
||||||
│
|
│
|
||||||
AES-256-GCM encrypt
|
AES-256-GCM encrypt
|
||||||
│
|
│
|
||||||
enc_data + iv ──► POST /api/vault
|
enc_data + iv (item payload + tags)
|
||||||
enc_name + iv_name (item name, encrypted separately)
|
enc_name + iv_name (item name)
|
||||||
|
│
|
||||||
|
POST /api/vault ──► Server stores ciphertext only
|
||||||
```
|
```
|
||||||
|
|
||||||
The server is blind to all vault content. A database breach exposes only encrypted ciphertext.
|
A database breach exposes only encrypted ciphertext. The server cannot read vault names, passwords, or tags.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -66,7 +79,7 @@ The server is blind to all vault content. A database breach exposes only encrypt
|
|||||||
| Frontend | Vanilla JS, Web Crypto API, Jinja2 |
|
| Frontend | Vanilla JS, Web Crypto API, Jinja2 |
|
||||||
| Auth | Argon2id + PBKDF2 + JWT (HS256) |
|
| Auth | Argon2id + PBKDF2 + JWT (HS256) |
|
||||||
| Encryption | AES-256-GCM (client-side) |
|
| Encryption | AES-256-GCM (client-side) |
|
||||||
| Extension | Chrome Manifest V3 |
|
| Extension | Chrome MV3 / Firefox MV2 |
|
||||||
| Web server | Nginx + Gunicorn + systemd |
|
| Web server | Nginx + Gunicorn + systemd |
|
||||||
| Rate limiting | Flask-Limiter + Redis |
|
| Rate limiting | Flask-Limiter + Redis |
|
||||||
| TLS | Let's Encrypt / Certbot |
|
| TLS | Let's Encrypt / Certbot |
|
||||||
@@ -76,136 +89,104 @@ The server is blind to all vault content. A database breach exposes only encrypt
|
|||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Python 3.12+
|
- Python 3.12+
|
||||||
- MySQL 8.x
|
- MySQL 8.x
|
||||||
- Node.js is **not** required — no build step
|
- No Node.js or build step required
|
||||||
|
|
||||||
### Development (Windows)
|
### Development (Windows)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Clone and set up virtual environment
|
|
||||||
git clone https://github.com/yourname/passkeeper
|
git clone https://github.com/yourname/passkeeper
|
||||||
cd passkeeper
|
cd passkeeper
|
||||||
python -m venv .venv
|
python -m venv .venv
|
||||||
.venv\Scripts\activate
|
.venv\Scripts\activate
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
# 2. Configure environment
|
|
||||||
copy .env.example .env
|
copy .env.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
Edit `.env`:
|
Edit `.env`:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
FLASK_ENV=development
|
FLASK_ENV=development
|
||||||
SECRET_KEY=your-secret-key
|
SECRET_KEY=<64-char hex>
|
||||||
JWT_SECRET_KEY=your-jwt-secret
|
JWT_SECRET_KEY=<64-char hex>
|
||||||
MYSQL_HOST=localhost
|
MYSQL_HOST=localhost
|
||||||
MYSQL_USER=passkeeper
|
MYSQL_USER=passkeeper
|
||||||
MYSQL_PASSWORD=yourpassword
|
MYSQL_PASSWORD=yourpassword
|
||||||
MYSQL_DB=passkeeper
|
MYSQL_DB=passkeeper
|
||||||
TOTP_ENCRYPTION_KEY=64-char-hex-string # python -c "import secrets; print(secrets.token_hex(32))"
|
TOTP_ENCRYPTION_KEY=<64-char hex>
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```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 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 "CREATE USER 'passkeeper'@'localhost' IDENTIFIED BY 'yourpassword'; GRANT ALL ON passkeeper.* TO 'passkeeper'@'localhost'; FLUSH PRIVILEGES;"
|
||||||
mysql -u root -p -e "GRANT ALL PRIVILEGES ON passkeeper.* TO 'passkeeper'@'localhost'; FLUSH PRIVILEGES;"
|
|
||||||
|
|
||||||
# 4. Create tables
|
|
||||||
python reset_db.py
|
python reset_db.py
|
||||||
|
|
||||||
# 5. Run
|
|
||||||
python run.py
|
python run.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Open `http://localhost:5000`.
|
Generate secrets:
|
||||||
|
```bash
|
||||||
|
python -c "import secrets; print(secrets.token_hex(32))"
|
||||||
|
```
|
||||||
|
|
||||||
### Load the Extension (Chrome)
|
### Load the Extension (Chrome)
|
||||||
|
1. `chrome://extensions/` → Enable **Developer mode**
|
||||||
|
2. **Load unpacked** → select `extension/` folder
|
||||||
|
3. Reload after any JS/CSS changes
|
||||||
|
|
||||||
1. Open `chrome://extensions/`
|
### Load the Extension (Firefox)
|
||||||
2. Enable **Developer mode** (top-right toggle)
|
1. `about:debugging` → **This Firefox** → **Load Temporary Add-on**
|
||||||
3. Click **Load unpacked** → select the `extension/` folder
|
2. Select `extension/manifest.firefox.json`
|
||||||
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
|
## Production Deployment
|
||||||
|
|
||||||
### 1. Server dependencies
|
### 1. Server dependencies
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt update && sudo apt install python3.12 python3.12-venv mysql-server nginx \
|
sudo apt update && sudo apt install python3.12 python3.12-venv mysql-server nginx \
|
||||||
certbot python3-certbot-nginx redis-server
|
certbot python3-certbot-nginx redis-server
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Application setup
|
### 2. Application setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /var/www/passkeeper
|
cd /var/www/passkeeper
|
||||||
python3.12 -m venv .venv
|
python3.12 -m venv .venv && source .venv/bin/activate
|
||||||
source .venv/bin/activate
|
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
cp .env.example .env # set production values
|
cp .env.example .env # set production values
|
||||||
flask db upgrade # run all migrations
|
flask db upgrade # run all migrations
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. systemd service
|
### 3. systemd service
|
||||||
|
|
||||||
Copy `scripts/passkeeper.service` to `/etc/systemd/system/passkeeper.service`, then:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo systemctl daemon-reload
|
sudo cp scripts/passkeeper.service /etc/systemd/system/
|
||||||
sudo systemctl enable --now passkeeper
|
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
|
### 4. Nginx
|
||||||
|
|
||||||
Copy `scripts/passkeeper-nginx.conf` to `/etc/nginx/sites-available/passkeeper`, update `server_name`, then:
|
|
||||||
|
|
||||||
```bash
|
```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 ln -s /etc/nginx/sites-available/passkeeper /etc/nginx/sites-enabled/
|
||||||
sudo certbot --nginx -d yourdomain.com
|
sudo certbot --nginx -d yourdomain.com
|
||||||
sudo nginx -t && sudo systemctl reload nginx
|
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. Future schema migrations
|
||||||
|
|
||||||
### 5. Schema migrations (future updates)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
flask db upgrade
|
flask db upgrade && sudo systemctl reload passkeeper
|
||||||
sudo systemctl reload passkeeper
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
| Variable | Description | Example |
|
| Variable | Description |
|
||||||
|---|---|---|
|
|---|---|
|
||||||
| `SECRET_KEY` | Flask session secret | 64-char random hex |
|
| `SECRET_KEY` | Flask session secret (64-char hex) |
|
||||||
| `JWT_SECRET_KEY` | JWT signing secret | 64-char random hex |
|
| `JWT_SECRET_KEY` | JWT signing secret (64-char hex) |
|
||||||
| `MYSQL_HOST` | MySQL host | `localhost` |
|
| `MYSQL_HOST` / `MYSQL_USER` / `MYSQL_PASSWORD` / `MYSQL_DB` | Database |
|
||||||
| `MYSQL_USER` | MySQL username | `passkeeper` |
|
| `TOTP_ENCRYPTION_KEY` | Server-side AES key for TOTP secrets (64-char hex) |
|
||||||
| `MYSQL_PASSWORD` | MySQL password | — |
|
| `RATELIMIT_STORAGE_URI` | Redis URI — required in production (`redis://127.0.0.1:6379/0`) |
|
||||||
| `MYSQL_DB` | Database name | `passkeeper` |
|
| `CORS_ORIGINS` | Allowed origins (`*` in dev, domain in prod) |
|
||||||
| `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))"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -213,64 +194,71 @@ python -c "import secrets; print(secrets.token_hex(32))"
|
|||||||
|
|
||||||
```
|
```
|
||||||
passkeeper/
|
passkeeper/
|
||||||
├── app/ # Flask application
|
├── app/ # Flask application
|
||||||
│ ├── models/ # SQLAlchemy models
|
│ ├── models/ # SQLAlchemy models
|
||||||
│ ├── routes/ # API blueprints (auth, vault, folders, sharing, emergency)
|
│ ├── routes/ # API blueprints (auth, vault, folders, sharing, emergency)
|
||||||
│ ├── services/ # Auth service (Argon2id, JWT, TOTP encryption)
|
│ ├── services/ # Auth (Argon2id, JWT, TOTP encryption)
|
||||||
│ ├── static/js/ # Client-side crypto + vault UI
|
│ ├── static/js/ # Client-side crypto + vault UI
|
||||||
│ └── templates/ # Jinja2 HTML templates
|
│ └── templates/ # Jinja2 templates
|
||||||
├── extension/ # Chrome extension (Manifest V3)
|
├── extension/ # Browser extension
|
||||||
│ ├── popup/ # Popup UI (HTML + CSS + JS)
|
│ ├── popup/ # Popup UI
|
||||||
│ ├── content/ # Content script (form detection, autofill icon, dropdown)
|
│ ├── content/ # Content script (autofill, field detection)
|
||||||
│ ├── shared/ # Shared crypto (vault key, encryptName/decryptName)
|
│ ├── shared/ # Shared crypto + Firefox polyfill
|
||||||
│ ├── bridge/ # SSO bridge (web app ↔ extension session sync)
|
│ ├── bridge/ # SSO bridge
|
||||||
│ └── background.js # Service worker (badges, message relay, idle lock)
|
│ ├── background.js # Chrome MV3 service worker
|
||||||
├── migrations/ # Alembic migration scripts
|
│ ├── background.firefox.js # Firefox MV2 background page
|
||||||
├── scripts/ # Nginx config, systemd unit, backup scripts
|
│ ├── manifest.json # Chrome/Edge MV3
|
||||||
├── reset_db.py # Dev-only: drop + recreate all tables
|
│ └── manifest.firefox.json # Firefox MV2
|
||||||
├── requirements.txt
|
├── migrations/ # Alembic migration scripts
|
||||||
├── run.py # Development server entry point
|
├── scripts/ # Nginx, systemd, backup
|
||||||
└── wsgi.py # Gunicorn entry point
|
└── requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## API Overview
|
## API Overview
|
||||||
|
|
||||||
All vault and folder endpoints require `Authorization: Bearer <access_token>`.
|
All vault/folder endpoints require `Authorization: Bearer <access_token>`.
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
| Method | Endpoint | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| POST | `/api/auth/register` | Create account |
|
| POST | `/api/auth/register` | Create account |
|
||||||
| POST | `/api/auth/login` | Authenticate; returns tokens or MFA challenge |
|
| POST | `/api/auth/login` | Authenticate |
|
||||||
| POST | `/api/auth/mfa/verify` | Complete MFA step |
|
| POST | `/api/auth/mfa/verify` | Complete MFA |
|
||||||
| POST | `/api/auth/refresh` | Rotate refresh token |
|
| POST | `/api/auth/refresh` | Rotate tokens |
|
||||||
| POST | `/api/auth/logout` | Blacklist both tokens |
|
| POST | `/api/auth/logout` | Blacklist tokens |
|
||||||
| GET | `/api/vault` | List all encrypted vault items |
|
| GET | `/api/vault` | List encrypted items |
|
||||||
| POST | `/api/vault` | Create item (`enc_data`, `iv`, `enc_name`, `iv_name`) |
|
| POST | `/api/vault` | Create item (`enc_data`, `iv`, `enc_name`, `iv_name`, tags in payload) |
|
||||||
| PUT | `/api/vault/<id>` | Update item |
|
| PUT | `/api/vault/<id>` | Update item |
|
||||||
| DELETE | `/api/vault/<id>` | Delete item |
|
| DELETE | `/api/vault/<id>` | Delete item |
|
||||||
| GET | `/api/folders` | List folders |
|
| 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 (ECDH re-encryption) |
|
| POST | `/api/sharing` | Share item (ECDH re-encryption) |
|
||||||
| POST | `/api/emergency` | Create emergency access grant |
|
| POST | `/api/emergency` | Create emergency access grant |
|
||||||
| POST | `/api/auth/change-password` | Atomic vault re-encryption on password change |
|
| POST | `/api/auth/change-password` | Atomic vault re-encryption |
|
||||||
| POST | `/api/auth/recover` | Account recovery (one-time use) |
|
| POST | `/api/auth/recover` | Account recovery (one-time) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
**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
|
## Backup
|
||||||
|
|
||||||
Automated MySQL backups with 30-day retention:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install cron job
|
# Install cron job
|
||||||
sudo cp scripts/passkeeper-logrotate /etc/logrotate.d/passkeeper
|
sudo cp scripts/passkeeper-logrotate /etc/logrotate.d/passkeeper
|
||||||
crontab scripts/backup.cron
|
crontab scripts/backup.cron
|
||||||
```
|
|
||||||
|
|
||||||
Manual backup:
|
# Manual backup
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/backup_db.sh
|
bash scripts/backup_db.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user