05/09 Remove conflicted files

This commit is contained in:
Nguyen Ngo
2026-05-09 15:01:33 -04:00
parent c054147018
commit b4de3e78d7
2 changed files with 0 additions and 804 deletions
@@ -1,516 +0,0 @@
# 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 (`<select id="web-idle-select">`).
### 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.
### Clipboard auto-clear
- Web app: `copyToClipboard()` uses `_clipboardClearTimer` setTimeout 30s → `writeText('')`.
- Extension: `_copyWithAutoClear()` same pattern. Applied to password, username, and flyout copies.
### Missing 2FA warning
- Security dashboard "No 2FA Saved" section: password items with URL but no `totp_uri`.
- Uses existing `extractTotpSecret()` and `makeSection()`.
### 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.
### Keyboard shortcut
- `manifest.json`: `commands._execute_action`, `Ctrl+Shift+L` / `Cmd+Shift+L`.
- `manifest.firefox.json`: `_execute_browser_action`.
- Customisable at `chrome://extensions/shortcuts`.
### 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 |
Load in Firefox: `about:debugging` → This Firefox → Load Temporary Add-on → `manifest.firefox.json`.
### `_normaliseUrl()` — bare-domain matching
```js
function _normaliseUrl(raw) {
if (!raw) return null;
const s = raw.trim();
if (/^https?:\/\//i.test(s)) return s;
if (s.startsWith("//")) return "https:" + s;
return "https://" + s;
}
```
In both `content.js` and `popup.js`. Prevents silent match failures for bare domains.
### `_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
### 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` debounced 150ms on `input`
`_debounce(fn, ms)` helper. `focus` listener remains instant.
### Three-dot flyout menu
`.pk-flyout` is rendered with `position: fixed`, appended to `#app` (not inside the button).
Coordinates are computed from `btn.getBoundingClientRect()` so it escapes `.vault-list`
`overflow-y: auto` clipping. Auto-flips upward when `spaceBelow < flyoutH + 8` — items near
the bottom of the list show the menu above the button, clearing the bottom nav bar.
Dynamic items: Open URL / Copy username / Copy password. Closes on outside click.
### Advanced Settings (password items)
Three boolean flags stored inside `enc_data` as part of `plain` — zero-knowledge, same
pattern as `plain.tags`. No database schema change required.
| Field | Default | Behaviour |
| ------|---------|---------- |
| `plain.autofill` | `true` | Whether the extension shows the fill button and fills this item |
| `plain.autologin` | `false` | After filling, automatically submit the form (`submitBtn.click()` with 400 ms delay) |
| `plain.reprompt` | `false` | Require master password before filling, copying, or editing |
**Web app (`vault.js` / `index.html`):**
- Collapsible `#adv-settings-toggle` / `#adv-settings-body` section in the password type-fields block.
- Starts **collapsed** (`hidden`) every time the modal opens (`openModal` resets state).
- `buildPlainData('password')` collects the three checkbox values.
- `openModal` edit-mode populates checkboxes from `plain.*`; `autofill` defaults `true` via HTML `checked` attribute — reset by `form.reset()`.
- CSS: `.adv-section`, `.adv-section-header`, `.adv-section-body`, `.adv-checkbox-row`, `.adv-checkbox-label`, `.adv-checkbox-desc` in `app.css`.
**Extension (`popup.js`):**
- `canFill` gates on `plain.autofill !== false` — hides the fill button for items with autofill disabled.
- Copy-password, copy-username, autofill button, and flyout copy actions all check `plain.reprompt`.
- `_repromptMasterPassword()`: full-screen overlay inside `#app`; derives vault key from entered password + `enc_key_salt`; compares JWK bytes to stored `vault_key_jwk`.
- Autofill message sends `autologin: !!plain.autologin` to the content script.
**Extension (`content.js`):**
- `doAutofill(username, password, autologin)` — when `autologin` is `true`, finds the closest form's submit button and clicks it (or calls `form.submit()`) after a 400 ms delay to let React/Vue input handlers fire.
- `DO_AUTOFILL` message handler forwards `!!msg.autologin`.
### AuditLog pattern
```python
db.session.add(item)
db.session.flush() # populates item.id
AuditLog.log(user_id=..., action='vault_item.create', resource_id=item.id, ...)
db.session.commit() # atomic
```
For deletes: capture `id` and `name` before flush.
### Common gotchas
- `item_type`: use `db.String(20)`, not `db.Enum(ItemType)` — enum lazy-load breaks `.value`
- `INTEGER(unsigned=True)`: requires `from sqlalchemy.dialects.mysql import INTEGER`
- All datetimes: naive UTC `datetime.utcnow()` — never mix with timezone-aware
- Extension vault key: `extractable: true` (web app uses `false`)
- PyJWT `sub`: `str(user_id)` on encode, `int(payload['sub'])` on decode
- 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
---
## Audit Action Catalog
| Module | Action | Trigger |
| -------------- | ------------------------------------------------------ | ------------------------ |
| `auth.py` | `auth.register` | New account |
| `auth.py` | `auth.login` / `auth.login_failed` | Login success/fail |
| `auth.py` | `auth.mfa_enable/disable/verify` | TOTP actions |
| `auth.py` | `auth.change_password` / `auth.change_password_failed` | Password change |
| `auth.py` | `auth.delete_account` / `auth.delete_account_failed` | Deletion |
| `auth.py` | `auth.recovery_setup/failed/items_denied/success` | Recovery |
| `vault.py` | `vault_item.create/update/delete` | CRUD |
| `vault.py` | `vault_item.export` / `vault_item.import` | Import/Export |
| `folders.py` | `folder.create/update/delete` | Folder CRUD |
| `sharing.py` | `sharing_keys.create/update` | ECDH key setup |
| `sharing.py` | `shared_item.create/delete/accept` | Sharing |
| `emergency.py` | `emergency_access.*` | All EA state transitions |
---
## REST API
```
POST /api/auth/register|login|logout|refresh|recover
GET /api/auth/me|mfa/status|mfa/setup|recovery/status|recovery/data|recovery/items
POST /api/auth/mfa/enable|disable|verify|change-password|recovery/setup
DELETE /api/auth/account
GET /api/vault # list all encrypted items
POST /api/vault # { name, item_type, folder_id, enc_data, iv, enc_name?, iv_name? }
GET /api/vault/<id>
PUT /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|POST /api/folders
PUT|DELETE /api/folders/<id>
GET|POST /api/sharing/keys
GET /api/sharing/public-key
GET|POST /api/sharing
DELETE /api/sharing/<id>
GET /api/sharing/inbox
POST /api/sharing/inbox/<id>/accept
GET|POST /api/emergency
DELETE /api/emergency/<id>
POST /api/emergency/<id>/accept|provide|request|deny
GET /api/emergency/<id>/vault
```
---
## Development Setup
```bash
python -m venv .venv && .venv\Scripts\activate
pip install -r requirements.txt
# .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;"
python reset_db.py
python run.py
```
## Production Deployment
```bash
flask db upgrade
sudo systemctl reload passkeeper
```
## Python Dependencies
```
flask>=3.0 flask-sqlalchemy>=3.1 flask-migrate>=4.0 flask-login>=0.6
flask-wtf>=1.2 flask-limiter>=3.5 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 # TOTP secret encryption
redis>=5.0 # Rate-limit storage (required in production)
```
---
## Notes
- Never log decrypted vault data server-side
- Tags live in `enc_data` as `plain.tags: string[]` — no schema change ever needed
- `vault_items_cs` is in `chrome.storage.session` — decrypted data never written to disk
- HIBP checks run progressively — synchronous sections render first, then parallel async checks
- Clipboard cleared 30 s after every password/username copy (web app + extension)
- Web-app idle timeout in `localStorage` (`web_idle_minutes`); default 15 min
- Browser back/forward works for all five vault views via `history.pushState`
- Pending save badge (`"!"`) set on `SAVE_CREDENTIALS`, cleared via `CLEAR_SAVE_BADGE`
- Firefox: use `manifest.firefox.json` + `background.firefox.js` + `browser-polyfill.js`
- Redis required in production for rate limiting
- Recovery code never stored server-side; password change clears it (user must regenerate)
- `save_blocklist` in `chrome.storage.local` suppresses save banner per hostname
---
## CSP & Nginx Header Architecture
### Why Nginx — not Flask — owns the authoritative CSP
The deployment stack is `Browser → Nginx → Gunicorn → Flask`. When Nginx emits a
`Content-Security-Policy` header with `always`, it **replaces** any CSP header
Flask emits upstream. Changes to `app/__init__.py` or `base.html` alone are
ineffective in production — **`scripts/passkeeper-nginx.conf` is the single source
of truth for CSP in production.**
### Nginx `add_header` inheritance rule (critical)
> Any `location` block that declares even one `add_header` directive silently drops
> **all** `add_header` directives from every parent block for that location.
The `/static/` block uses `add_header Cache-Control`. Without explicitly repeating
all security headers inside that block, every static asset (`vault.js`, `app.css`,
etc.) is served with **no** CSP, no HSTS, no `X-Frame-Options` — none.
**All security headers must be present in both the `server` block and the
`/static/` location block.** Keep them in sync whenever either is modified.
### Approved external `connect-src` origins
| Origin | Purpose |
| -------------------------------- | -------------------------------------------------- |
| `https://api.pwnedpasswords.com` | HIBP k-anonymity breach check (Security Dashboard) |
`connect-src 'self' https://api.pwnedpasswords.com` must appear in the CSP in
**both** the `server` block and the `/static/` location block in
`passkeeper-nginx.conf`.
### Flask-side CSP (`app/__init__.py` + `base.html`)
Flask also sets a CSP header and `base.html` has a `<meta http-equiv>` CSP tag.
Keep these in sync with the Nginx config for correctness in development (where
Nginx is not present). Note:
- `frame-ancestors` is valid **only** in HTTP headers — never in `<meta>` CSP tags.
The browser silently ignores it in meta tags and logs a warning.
- The `<meta>` tag CSP does not enforce `frame-ancestors`; the Nginx HTTP header does.
### Inline event handler prohibition
`script-src 'self'` blocks all inline `on*` HTML attributes. Do not add `onclick`,
`onchange`, or any other inline handler to any template. Wire all interactions via
`addEventListener` in the corresponding JS file instead.
The `Vault.switchToImportExport()` export on the `vault.js` public API exists for
historical reasons. The `#sidebar-import-export` element is wired via
`addEventListener` in `init()` — the exported function is not needed for new code.
### Third-party browser extension noise
Console errors referencing `isCheckout`, `content-script.js`, or the extension ID
`clmkdohmabikagpnhjmgacbclihgmdje` originate from a third-party shopping/coupon
browser extension — not from PassKeeper. These can be ignored.
The `[PassKeeper] decorateFields: host=…, matched=0 of 0 items` log from
`content.js` is a routine debug message (not an error): the content script found
no stored credentials matching the current hostname, which is expected on the vault
page itself.
@@ -1,288 +0,0 @@
# 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
- **Advanced Settings per password item** — Autofill (on/off), Autologin (auto-submit form), Reprompt (re-verify master password before use); all stored encrypted inside `enc_data` — zero-knowledge, no schema change
- **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; share with any registered user
- **Emergency access** — configurable wait-timer access grant for a trusted contact
- **Security dashboard** — weak / reused / old / **no 2FA saved** / **HaveIBeenPwned breach check** (k-anonymity — passwords never transmitted)
- **Import / Export** — encrypted JSON backup; CSV export (plaintext, handle carefully); import from Chrome, Bitwarden, and 1Password CSV formats
- **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
- **Audit log** — server-side trail of all create/edit/delete/import/export actions
- **Encrypted item names** — `enc_name`/`iv_name`; server holds only the item type as a label
- **Browser history** — back/forward button works for all views (`history.pushState`)
- **Web-app auto-lock** — configurable inactivity timeout (5/10/15/30/60 min or Never); stored per browser in `localStorage`
- **Clipboard auto-clear** — sensitive copies cleared after 30 seconds
- **Responsive layout** — phone, tablet, laptop, large desktop; collapsible sidebar
### Browser Extension (Chrome / Edge — Manifest V3 + Firefox — Manifest V2)
- **Autofill** — detects login forms; injects icon into username and password fields only (scored heuristic, not all inputs)
- **Advanced Settings enforcement** — respects per-item settings from the web vault:
- **Autofill off** — hides the fill button; item still appears in the list but cannot be auto-filled
- **Autologin** — after filling, automatically clicks the submit button (400 ms delay for SPA compatibility)
- **Reprompt** — shows a master-password overlay before filling, copying, or accessing the item
- **Smart domain matching** — matches by domain name; handles bare domains (`github.com`) and subdomains
- **Suggestion dropdown** — filters as you type; keyboard navigation (↑↓ Enter); one-click fill
- **Collapsible folder groups** — "All items" tab groups by folder with collapse/expand toggle matching the web app UX
- **Favorites tab** — items tagged `favorite` in the web app appear in a dedicated tab; marked with ★
- **Tag display** — purple badge pills on item rows; tags sourced from `plain.tags[]`
- **Three-dot flyout menu** — contextual actions: Open URL / Copy username / Copy password; rendered with `position: fixed` to escape list overflow clipping; auto-flips above button when near bottom nav
- **Dedicated copy-username button** — person icon alongside copy-password in every item row
- **Auto-save banner** — save/update credentials on form submit; duplicate detection; "Never for this site" blocklist
- **Pending-save badge** — red `!` on toolbar icon when a credential is waiting to be saved
- **Inline password generator** — fully CSPRNG (`crypto.getRandomValues` throughout, `Math.random` never called)
- **TOTP live display** — 6-digit code + countdown per item
- **SSO bridge** — log in once on the web app; extension picks up the session
- **Idle lock** — configurable auto-lock (1/5/10/30 min or Never)
- **Clipboard auto-clear** — passwords/usernames cleared from clipboard after 30 seconds
- **Keyboard shortcut** — `Ctrl+Shift+L` / `Cmd+Shift+L` to open popup (customisable at `chrome://extensions/shortcuts`)
- **Firefox compatible** — separate MV2 manifest + background script; same popup/content/bridge code
---
## Security Architecture
```
Master Password
├─ PBKDF2(email, 100k iter) ──► authHash ──► POST /api/auth/login
│ Argon2id(authHash) stored in DB
└─ 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
```
A database breach exposes only encrypted ciphertext. The server cannot read vault names, passwords, or tags.
---
## 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) |
| Encryption | AES-256-GCM (client-side) |
| Extension | Chrome MV3 / Firefox MV2 |
| Web server | Nginx + Gunicorn + systemd |
| Rate limiting | Flask-Limiter + Redis |
| TLS | Let's Encrypt / Certbot |
---
## Getting Started
### Prerequisites
- Python 3.12+
- MySQL 8.x
- No Node.js or build step required
### Development (Windows)
```bash
git clone https://github.com/yourname/passkeeper
cd passkeeper
python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txt
copy .env.example .env
```
Edit `.env`:
```env
FLASK_ENV=development
SECRET_KEY=<64-char hex>
JWT_SECRET_KEY=<64-char hex>
MYSQL_HOST=localhost
MYSQL_USER=passkeeper
MYSQL_PASSWORD=yourpassword
MYSQL_DB=passkeeper
TOTP_ENCRYPTION_KEY=<64-char hex>
```
```bash
mysql -u root -p -e "CREATE DATABASE passkeeper CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mysql -u root -p -e "CREATE USER 'passkeeper'@'localhost' IDENTIFIED BY 'yourpassword'; GRANT ALL ON passkeeper.* TO 'passkeeper'@'localhost'; FLUSH PRIVILEGES;"
python reset_db.py
python run.py
```
Generate secrets:
```bash
python -c "import secrets; print(secrets.token_hex(32))"
```
### Load the Extension (Chrome)
1. `chrome://extensions/` → Enable **Developer mode**
2. **Load unpacked** → select `extension/` folder
3. Reload after any JS/CSS changes
### Load the Extension (Firefox)
1. `about:debugging`**This Firefox****Load Temporary Add-on**
2. Select `extension/manifest.firefox.json`
---
## Production Deployment
### 1. Server dependencies
```bash
sudo apt update && sudo apt install python3.12 python3.12-venv mysql-server nginx \
certbot python3-certbot-nginx redis-server
```
### 2. Application setup
```bash
cd /var/www/passkeeper
python3.12 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # set production values
flask db upgrade # run all migrations
```
### 3. systemd service
```bash
sudo cp scripts/passkeeper.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now passkeeper
```
### 4. Nginx
```bash
sudo cp scripts/passkeeper-nginx.conf /etc/nginx/sites-available/passkeeper
sudo ln -s /etc/nginx/sites-available/passkeeper /etc/nginx/sites-enabled/
sudo certbot --nginx -d yourdomain.com
sudo nginx -t && sudo systemctl reload nginx
```
### 5. Future schema migrations
```bash
flask db upgrade && sudo systemctl reload passkeeper
```
---
## Environment Variables
| Variable | Description |
|---|---|
| `SECRET_KEY` | Flask session secret (64-char hex) |
| `JWT_SECRET_KEY` | JWT signing secret (64-char hex) |
| `MYSQL_HOST` / `MYSQL_USER` / `MYSQL_PASSWORD` / `MYSQL_DB` | Database |
| `TOTP_ENCRYPTION_KEY` | Server-side AES key for TOTP secrets (64-char hex) |
| `RATELIMIT_STORAGE_URI` | Redis URI — required in production (`redis://127.0.0.1:6379/0`) |
| `CORS_ORIGINS` | Allowed origins (`*` in dev, domain in prod) |
---
## Project Structure
```
passkeeper/
├── app/ # Flask application
│ ├── models/ # SQLAlchemy models
│ ├── routes/ # API blueprints (auth, vault, folders, sharing, emergency)
│ ├── services/ # Auth (Argon2id, JWT, TOTP encryption)
│ ├── 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 |
| 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 (ECDH re-encryption) |
| POST | `/api/emergency` | Create emergency access grant |
| POST | `/api/auth/change-password` | Atomic vault re-encryption |
| 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.
---
## Advanced Settings (Password Items)
Three optional boolean fields stored inside `enc_data` alongside credentials — zero-knowledge, no schema change required.
| Setting | Default | Effect |
|---|---|---|
| **Autofill** | ✅ Enabled | Extension shows the fill button; disabling hides it (item still visible in the list) |
| **Autologin** | ☐ Off | After filling credentials, the extension automatically submits the login form |
| **Reprompt** | ☐ Off | Before filling or copying, a master-password overlay must be passed |
Configure in the **Advanced Settings** collapsible at the bottom of the password item modal in the web vault. The section starts collapsed to keep the UI clean.
---
## Backup
```bash
# Install cron job
sudo cp scripts/passkeeper-logrotate /etc/logrotate.d/passkeeper
crontab scripts/backup.cron
# Manual backup
bash scripts/backup_db.sh
```
---
## Licence
MIT — see `LICENSE`.