05/18 Update CLAUDE.md and README.md

This commit is contained in:
2026-05-18 12:22:15 -04:00
parent 2eaf2adbf1
commit 09d3bbdc15
2 changed files with 164 additions and 54 deletions
+142 -43
View File
@@ -48,8 +48,10 @@ passkeeper/
│ │ ├── vault_item.py # enc_data, iv, enc_name, iv_name │ │ ├── vault_item.py # enc_data, iv, enc_name, iv_name
│ │ ├── folder.py │ │ ├── folder.py
│ │ ├── token_blacklist.py # JWT revocation (jti + expires_at) │ │ ├── token_blacklist.py # JWT revocation (jti + expires_at)
│ │ ├── shared_item.py # ECDH-encrypted cross-user shares │ │ ├── totp_used_code.py # TOTP replay prevention (one-time use per user)
│ │ ├── shared_item.py # ECDH-encrypted cross-user shares; enc_name/iv_name
│ │ ├── emergency_access.py # State machine │ │ ├── emergency_access.py # State machine
│ │ ├── recovery_challenge.py # Server-side recovery challenge (multi-worker safe)
│ │ └── audit_log.py │ │ └── audit_log.py
│ ├── routes/ │ ├── routes/
│ │ ├── auth.py # Register, login, MFA, logout, refresh, change-password, recovery │ │ ├── auth.py # Register, login, MFA, logout, refresh, change-password, recovery
@@ -58,7 +60,8 @@ passkeeper/
│ │ ├── sharing.py │ │ ├── sharing.py
│ │ └── emergency.py │ │ └── emergency.py
│ ├── services/ │ ├── services/
│ │ └── auth_service.py # Argon2id, JWT, blacklist, @require_jwt, TOTP encrypt/decrypt │ │ └── auth_service.py # Argon2id, JWT, blacklist, @require_jwt, TOTP encrypt/decrypt,
│ │ # TOTP replay helpers (is_totp_code_used / mark_totp_code_used)
│ ├── static/ │ ├── static/
│ │ ├── css/app.css # Full responsive stylesheet; collapsible group styles; tag badge styles │ │ ├── css/app.css # Full responsive stylesheet; collapsible group styles; tag badge styles
│ │ └── js/ │ │ └── js/
@@ -66,13 +69,13 @@ passkeeper/
│ │ │ # encryptName, decryptName, generateSalt │ │ │ # encryptName, decryptName, generateSalt
│ │ ├── auth.js # Login/register + TOTP MFA + VaultSession │ │ ├── auth.js # Login/register + TOTP MFA + VaultSession
│ │ ├── recover.js │ │ ├── recover.js
│ │ ├── sharing.js # ECDH P-256 │ │ ├── sharing.js # ECDH P-256; encryptName/decryptName for shared item names
│ │ └── vault.js # Full vault UI — see "Key Features" below │ │ └── vault.js # Full vault UI — see "Key Features" below
│ └── templates/vault/ │ └── templates/vault/
│ └── index.html # Tags field, Auto-Lock setting, Import/Export view + sidebar │ └── index.html # Tags field, Auto-Lock setting, Import/Export view + sidebar
├── extension/ ├── extension/
│ ├── manifest.json # MV3 (Chrome/Edge); Ctrl+Shift+L keyboard shortcut │ ├── manifest.json # MV3 (Chrome/Edge); web_accessible_resources: []
│ ├── manifest.firefox.json # MV2 (Firefox) │ ├── manifest.firefox.json # MV2 (Firefox); web_accessible_resources: []
│ ├── background.js # Chrome SW: badges, pending-save "!" badge, CLEAR_SAVE_BADGE │ ├── background.js # Chrome SW: badges, pending-save "!" badge, CLEAR_SAVE_BADGE
│ ├── background.firefox.js # Firefox: in-memory session shim, setTimeout idle lock │ ├── background.firefox.js # Firefox: in-memory session shim, setTimeout idle lock
│ ├── shared/ │ ├── shared/
@@ -94,7 +97,11 @@ passkeeper/
│ ├── 71d7158dd3b9_add_audit_log_tables_fix_sharing_public_.py │ ├── 71d7158dd3b9_add_audit_log_tables_fix_sharing_public_.py
│ ├── a1b2c3d4e5f6_encrypt_totp_secret_at_rest.py │ ├── a1b2c3d4e5f6_encrypt_totp_secret_at_rest.py
│ ├── b2c3d4e5f6a7_add_account_recovery_columns.py │ ├── b2c3d4e5f6a7_add_account_recovery_columns.py
── c3d4e5f6a7b8_encrypt_vault_item_name.py # adds enc_name + iv_name ── c3d4e5f6a7b8_encrypt_vault_item_name.py
│ ├── d4e5f6a7b8c9_add_lockout_and_mfa_backup_codes.py
│ ├── e5f6a7b8c9d0_add_recovery_challenges_table.py
│ ├── f6a7b8c9d0e1_add_totp_used_codes_table.py # TOTP replay prevention
│ └── g7h8i9j0k1l2_encrypt_shared_item_name.py # enc_name/iv_name on shared_items
├── scripts/ ├── scripts/
│ ├── reencrypt_totp_secrets.py │ ├── reencrypt_totp_secrets.py
│ ├── backup_db.sh / backup.cron / passkeeper-logrotate │ ├── backup_db.sh / backup.cron / passkeeper-logrotate
@@ -120,6 +127,7 @@ CREATE TABLE users (
totp_secret VARCHAR(255), -- AES-256-GCM ciphertext totp_secret VARCHAR(255), -- AES-256-GCM ciphertext
totp_iv VARCHAR(64), totp_iv VARCHAR(64),
totp_enabled TINYINT(1) DEFAULT 0, totp_enabled TINYINT(1) DEFAULT 0,
mfa_backup_codes TEXT, -- JSON array of Argon2id-hashed codes
sharing_public_key VARCHAR(128), sharing_public_key VARCHAR(128),
sharing_private_key_enc TEXT, sharing_private_key_enc TEXT,
sharing_private_key_iv VARCHAR(64), sharing_private_key_iv VARCHAR(64),
@@ -143,19 +151,61 @@ CREATE TABLE vault_items (
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)
-- Shared Items
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, -- non-sensitive fallback label (= item_type)
item_type VARCHAR(20) NOT NULL DEFAULT 'password',
enc_data TEXT NOT NULL, -- ECDH-encrypted item payload
iv VARCHAR(64) NOT NULL,
enc_name VARCHAR(512), -- ECDH-encrypted display name (nullable for legacy)
iv_name VARCHAR(64),
accepted TINYINT(1) DEFAULT 0,
created_at DATETIME NOT NULL
);
-- TOTP Used Codes (replay prevention)
CREATE TABLE totp_used_codes (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL,
code VARCHAR(6) NOT NULL,
expires_at DATETIME NOT NULL,
UNIQUE KEY uq_totp_used_user_code (user_id, code),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Recovery Challenges (multi-worker safe)
CREATE TABLE recovery_challenges (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL UNIQUE,
nonce VARCHAR(64) NOT NULL,
expected_proof VARCHAR(64) NOT NULL,
expires_at DATETIME NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- (folders, token_blacklist, emergency_access, audit_logs — standard schemas)
``` ```
--- ---
## Migration History ## Migration History
| Revision | Description | | Revision | Description |
| -------------- | ------------------------------------------- | | ---------------- | ------------------------------------------------ |
| `71d7158dd3b9` | Add audit_logs; fix sharing_public_key type | | `71d7158dd3b9` | Add audit_logs; fix sharing_public_key type |
| `a1b2c3d4e5f6` | Encrypt TOTP secret at rest | | `a1b2c3d4e5f6` | Encrypt TOTP secret at rest |
| `b2c3d4e5f6a7` | Add account recovery columns | | `b2c3d4e5f6a7` | Add account recovery columns |
| `c3d4e5f6a7b8` | Add enc_name + iv_name to vault_items | | `c3d4e5f6a7b8` | Add enc_name + iv_name to vault_items |
| `d4e5f6a7b8c9` | Add lockout columns + MFA backup codes |
| `e5f6a7b8c9d0` | Add recovery_challenges table |
| `f6a7b8c9d0e1` | Add totp_used_codes table (TOTP replay prevent.) |
| `g7h8i9j0k1l2` | Add enc_name/iv_name to shared_items |
--- ---
@@ -165,16 +215,20 @@ CREATE TABLE vault_items (
- `authHash = PBKDF2(masterPassword, email, 100k iter)` → auth only - `authHash = PBKDF2(masterPassword, email, 100k iter)` → auth only
- `vaultKey = PBKDF2(masterPassword, enc_key_salt, 600k iter)` → browser memory only - `vaultKey = PBKDF2(masterPassword, enc_key_salt, 600k iter)` → browser memory only
- All vault data (payload + name + tags) encrypted client-side (AES-256-GCM) - All vault data (payload + name + tags) encrypted client-side (AES-256-GCM)
- **Item name:** `enc_name`/`iv_name`; server `name` column = item type only - **Item name:** `enc_name`/`iv_name` in `vault_items`; server `name` column = item type only
- **Shared item name:** `enc_name`/`iv_name` encrypted with ECDH shared key; server `item_name` = item type only
- **Tags:** `plain.tags: string[]` inside `enc_data`; server never sees them - **Tags:** `plain.tags: string[]` inside `enc_data`; server never sees them
- **Argon2id:** double-hashes `authHash` server-side - **Argon2id:** double-hashes `authHash` server-side; transparently rehashes on login if parameters are upgraded
- **JWT:** HS256, 15 min access / 7 day refresh, JTI blacklisted on logout - **JWT:** HS256, 15 min access / 7 day refresh, JTI blacklisted on logout
- **MFA:** TOTP secret AES-256-GCM encrypted at rest - **MFA:** TOTP secret AES-256-GCM encrypted at rest; each code is single-use (replay prevented via `totp_used_codes` table, 120s TTL)
- **Sharing:** ECDH P-256 zero-knowledge re-encryption - **Sharing:** ECDH P-256 zero-knowledge re-encryption; item name also encrypted with shared key
- **Password generator:** fully CSPRNG (`_cryptoRandInt` rejection-sampling) - **Password generator:** fully CSPRNG (`_cryptoRandInt` rejection-sampling)
- **Decrypted vault data:** `chrome.storage.session` only — never to disk - **Decrypted vault data:** `chrome.storage.session` only — never to disk
- **Clipboard auto-clear:** 30 s after any password/username copy (web + extension) - **Clipboard auto-clear:** 30 s after any password/username copy (web + extension)
- **Breach detection:** HIBP k-anonymity — only 5-char SHA-1 prefix transmitted - **Breach detection:** HIBP k-anonymity — only 5-char SHA-1 prefix transmitted
- **Account recovery:** challenge-response via HMAC-SHA256; `enc_key_salt` NOT returned by `/recovery/data` — client must derive it by decrypting the recovery blob (proves possession of recovery code without transmitting it); challenge rotated on each `/recovery/items` call to prevent proof replay
- **folder_id ownership:** validated server-side on all create/update/import operations — user cannot assign items to another user's folder
- **Audit logs:** never contain plaintext item names, shared item names, or vault data
--- ---
@@ -198,12 +252,41 @@ Requires Chrome 111+. `content.js` `onChanged` listener watches `area === 'sessi
## Key Implementation Details ## Key Implementation Details
### TOTP replay prevention
Every accepted TOTP code is recorded in `totp_used_codes` (user_id + code, 120s TTL). A second attempt with the same code within that window returns `400 Verification code already used`. Applies to: `mfa_enable`, `mfa_disable`, `mfa_verify`, `mfa_backup_codes_regenerate`. The table is pruned by the APScheduler cleanup job alongside `token_blacklist` and `recovery_challenges`.
### Argon2 transparent rehash
`verify_auth_token(auth_hash, stored_hash, user=user)` calls `ph.check_needs_rehash()` on success. If the stored hash uses outdated parameters (e.g. after raising `ARGON2_TIME_COST`), the hash is silently upgraded in the same DB commit as the login success. Pass `user=user` at all login callsites.
### folder_id ownership validation
`_validate_folder_id(folder_id, user_id)` in `vault.py` queries `Folder` by both `id` and `user_id`. Returns sanitised int or `None`. Raises `ValueError` on mismatch. Applied in `create_item` (400 on invalid), `update_item` (400 on invalid), `import_items` (silently clears to `None` — item still imports to root).
### Account recovery flow
```
1. GET /recovery/data → server creates challenge; returns nonce + recovery blob
enc_key_salt NOT returned (client must decrypt blob)
2. Client decrypts blob with recovery code → gets enc_key_salt
3. Client computes proof = HMAC-SHA256(enc_key_salt, nonce)
4. GET /recovery/items → validates proof; consumes challenge; re-issues fresh challenge
with same expected_proof + new nonce (prevents proof replay)
5. POST /recover → validates proof again; consumes rotated challenge; atomically
re-encrypts vault + resets password
```
### Shared item name encryption
When creating a share, the client encrypts `item.name` with `SharingCrypto.encryptName(sharedKey, name)``enc_name`/`iv_name`. The server receives `item_name = item.item_type` (non-sensitive type label) for the `NOT NULL` column. On the recipient's inbox, `enc_name`/`iv_name` are stored in data attributes on the View button and decrypted client-side with `SharingCrypto.decryptName()` when the user clicks View. Legacy shares (pre-migration, no `enc_name`) fall back to showing `item_name` (the type string).
### Vault item tags ### Vault item tags
- Stored as `plain.tags: string[]` inside `enc_data`. No schema change ever needed. - 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. - **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. - **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. - `handleSearch()` pool correctly branches on `itemType` / `folder` / `tag` filter types.
### Collapsible folder groups ### Collapsible folder groups
@@ -215,8 +298,13 @@ Requires Chrome 111+. `content.js` `onChanged` listener watches `area === 'sessi
- **Encrypted JSON export:** `GET /api/vault` → versioned envelope → download. Zero-knowledge. - **Encrypted JSON export:** `GET /api/vault` → versioned envelope → download. Zero-knowledge.
- **CSV export:** decrypt client-side → `name, url, username, password, notes`. - **CSV export:** decrypt client-side → `name, url, username, password, notes`.
- **JSON import:** sends encrypted blobs to `POST /api/vault/import` directly. - **JSON import:** sends encrypted blobs to `POST /api/vault/import` directly.
- **CSV import:** parses Chrome / Bitwarden / 1Password formats; encrypts client-side before POSTing. - **CSV import:** RFC 4180-compliant parser (handles `""` embedded quotes); parses Chrome / Bitwarden / 1Password formats; encrypts client-side before POSTing.
- Both endpoints write audit logs: `vault_item.export`, `vault_item.import`. - Both endpoints write audit logs: `vault_item.export`, `vault_item.import`.
- Import view resets state (file input, preview, result) every time the view is entered.
### `apiFetch` error handling
`apiFetch` throws on **all** non-ok responses including 404. Thrown error carries `e.status` for callers that need to branch. Callers should use `try/catch` rather than checking `res.ok` after the call.
### Web-app session timeout ### Web-app session timeout
@@ -262,6 +350,10 @@ Requires Chrome 111+. `content.js` `onChanged` listener watches `area === 'sessi
Load in Firefox: `about:debugging` → This Firefox → Load Temporary Add-on → `manifest.firefox.json`. Load in Firefox: `about:debugging` → This Firefox → Load Temporary Add-on → `manifest.firefox.json`.
### `web_accessible_resources`
Both manifests declare `web_accessible_resources: []` (MV3) / `[]` (MV2). This prevents any external page from loading extension resources via `chrome-extension://` or `moz-extension://` URLs, blocking extension fingerprinting.
### `_normaliseUrl()` — bare-domain matching ### `_normaliseUrl()` — bare-domain matching
```js ```js
@@ -300,40 +392,47 @@ Inspects added/removed nodes — if all carry `__pk` prefix, returns early. Prev
```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,
detail=f'Created {item_type} item (id={item.id})') # NO plaintext name
db.session.commit() # atomic db.session.commit() # atomic
``` ```
For deletes: capture `id` and `name` before flush. Audit log details **never** contain plaintext item names, shared item names, or any decrypted vault data. Use `item_type` and `id` only.
### Common gotchas ### Common gotchas
- `item_type`: use `db.String(20)`, not `db.Enum(ItemType)` — enum lazy-load breaks `.value` - `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` - `INTEGER(unsigned=True)`: requires `from sqlalchemy.dialects.mysql import INTEGER`
- All datetimes: naive UTC `datetime.utcnow()` — never mix with timezone-aware - All datetimes: naive UTC `datetime.now(timezone.utc).replace(tzinfo=None)` — never mix with timezone-aware
- Extension vault key: `extractable: true` (web app uses `false`) - Extension vault key: `extractable: true` (web app uses `false`)
- PyJWT `sub`: `str(user_id)` on encode, `int(payload['sub'])` on decode - PyJWT `sub`: `str(user_id)` on encode, `int(payload['sub'])` on decode
- TOTP key generation: `python -c "import secrets; print(secrets.token_hex(32))"` - 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 - `#vault-list` ID must not be renamed — `vault.js` renders into it directly
- `_validate_folder_id` must be called for any user-supplied `folder_id` before DB write
- `verify_auth_token` must receive `user=user` at login to enable Argon2 rehash
- APScheduler cleanup job handles `TokenBlacklist`, `RecoveryChallenge`, AND `TotpUsedCode`
--- ---
## Audit Action Catalog ## Audit Action Catalog
| Module | Action | Trigger | | Module | Action | Trigger |
| -------------- | ------------------------------------------------------ | ------------------------ | | -------------- | ------------------------------------------------------ | ------------------------------ |
| `auth.py` | `auth.register` | New account | | `auth.py` | `auth.register` | New account |
| `auth.py` | `auth.login` / `auth.login_failed` | Login success/fail | | `auth.py` | `auth.login` / `auth.login_failed` | Login success/fail |
| `auth.py` | `auth.mfa_enable/disable/verify` | TOTP actions | | `auth.py` | `auth.account_locked` | Failed login lockout |
| `auth.py` | `auth.change_password` / `auth.change_password_failed` | Password change | | `auth.py` | `auth.mfa_enable/disable/verify` | TOTP actions |
| `auth.py` | `auth.delete_account` / `auth.delete_account_failed` | Deletion | | `auth.py` | `auth.mfa_backup_code_used` | Backup code login |
| `auth.py` | `auth.recovery_setup/failed/items_denied/success` | Recovery | | `auth.py` | `auth.mfa_backup_codes_regenerated` | Backup code regen |
| `vault.py` | `vault_item.create/update/delete` | CRUD | | `auth.py` | `auth.change_password` / `auth.change_password_failed` | Password change |
| `vault.py` | `vault_item.export` / `vault_item.import` | Import/Export | | `auth.py` | `auth.delete_account` / `auth.delete_account_failed` | Deletion |
| `folders.py` | `folder.create/update/delete` | Folder CRUD | | `auth.py` | `auth.recovery_setup/failed/items_denied/success` | Recovery |
| `sharing.py` | `sharing_keys.create/update` | ECDH key setup | | `vault.py` | `vault_item.create/update/delete` | CRUD (detail: type + id only) |
| `sharing.py` | `shared_item.create/delete/accept` | Sharing | | `vault.py` | `vault_item.export` / `vault_item.import` | Import/Export |
| `emergency.py` | `emergency_access.*` | All EA state transitions | | `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 (detail: type + id) |
| `emergency.py` | `emergency_access.*` | All EA state transitions |
--- ---
@@ -342,11 +441,11 @@ For deletes: capture `id` and `name` before flush.
``` ```
POST /api/auth/register|login|logout|refresh|recover POST /api/auth/register|login|logout|refresh|recover
GET /api/auth/me|mfa/status|mfa/setup|recovery/status|recovery/data|recovery/items 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 POST /api/auth/mfa/enable|disable|verify|backup-codes/regenerate|change-password|recovery/setup
DELETE /api/auth/account DELETE /api/auth/account
GET /api/vault # list all encrypted items 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> PUT /api/vault/<id>
DELETE /api/vault/<id> DELETE /api/vault/<id>
@@ -358,9 +457,9 @@ PUT|DELETE /api/folders/<id>
GET|POST /api/sharing/keys GET|POST /api/sharing/keys
GET /api/sharing/public-key GET /api/sharing/public-key
GET|POST /api/sharing GET|POST /api/sharing # POST: { item_id, recipient_email, enc_data, iv,
DELETE /api/sharing/<id> DELETE /api/sharing/<id> # item_name (=item_type), item_type,
GET /api/sharing/inbox GET /api/sharing/inbox # enc_name, iv_name }
POST /api/sharing/inbox/<id>/accept POST /api/sharing/inbox/<id>/accept
GET|POST /api/emergency GET|POST /api/emergency
@@ -404,7 +503,7 @@ redis>=5.0 # Rate-limit storage (required in production)
## Notes ## Notes
- Never log decrypted vault data server-side - Never log decrypted vault data server-side — audit details use `item_type` + `id` only
- Tags live in `enc_data` as `plain.tags: string[]` — no schema change ever needed - 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 - `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 - HIBP checks run progressively — synchronous sections render first, then parallel async checks
@@ -480,4 +579,4 @@ browser extension — not from PassKeeper. These can be ignored.
The `[PassKeeper] decorateFields: host=…, matched=0 of 0 items` log from 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 `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 no stored credentials matching the current hostname, which is expected on the vault
page itself. page itself.
+22 -11
View File
@@ -13,14 +13,14 @@ A self-hosted, zero-knowledge password manager — web app and Chrome/Firefox ex
- **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 - **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 - **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 - **Folder organisation** — create, rename, delete; filter vault by folder
- **Item sharing** — ECDH P-256 zero-knowledge re-encryption; share with any registered user - **Item sharing** — ECDH P-256 zero-knowledge re-encryption; item name encrypted with shared key — server never sees it
- **Emergency access** — configurable wait-timer access grant for a trusted contact - **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) - **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 - **Import / Export** — encrypted JSON backup; CSV export (plaintext, handle carefully); import from Chrome, Bitwarden, and 1Password CSV formats (RFC 4180 compliant parser)
- **Account MFA** — TOTP-based login (Google Authenticator / Authy) - **Account MFA** — TOTP-based login (Google Authenticator / Authy); single-use code enforcement prevents replay attacks
- **Master password change** — atomic zero-knowledge re-encryption of entire vault including item names - **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; challenge-response proof prevents forgery
- **Audit log** — server-side trail of all create/edit/delete/import/export actions - **Audit log** — server-side trail of all create/edit/delete/import/export actions; no plaintext names ever logged
- **Encrypted item names** — `enc_name`/`iv_name`; server holds only the item type as a label - **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`) - **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` - **Web-app auto-lock** — configurable inactivity timeout (5/10/15/30/60 min or Never); stored per browser in `localStorage`
@@ -55,6 +55,7 @@ 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
│ (transparently rehashed if params upgraded)
└─ PBKDF2(enc_key_salt, 600k iter) ──► vaultKey (browser memory only) └─ PBKDF2(enc_key_salt, 600k iter) ──► vaultKey (browser memory only)
@@ -64,9 +65,19 @@ Master Password
enc_name + iv_name (item name) enc_name + iv_name (item name)
POST /api/vault ──► Server stores ciphertext only POST /api/vault ──► Server stores ciphertext only
Sharing: ECDH(Alice_priv, Bob_pub) ──► sharedKey ──► AES-256-GCM(enc_data + enc_name)
Server stores only ciphertext — cannot read item content or item name
``` ```
A database breach exposes only encrypted ciphertext. The server cannot read vault names, passwords, or tags. ### Security hardening highlights
- **TOTP replay prevention** — each 6-digit code is single-use (120s window); recorded in `totp_used_codes` table
- **Recovery proof** — `enc_key_salt` not returned by server during recovery; client must decrypt the recovery blob to prove code possession
- **folder_id ownership** — all create/update/import operations validate folder belongs to current user
- **Audit log privacy** — item names and shared item names never appear in server-side audit logs
- **Extension fingerprinting** — `web_accessible_resources: []` blocks external pages from probing extension files
A database breach exposes only encrypted ciphertext. The server cannot read vault names, passwords, tags, or shared item names.
--- ---
@@ -197,7 +208,7 @@ 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 (Argon2id, JWT, TOTP encryption) │ ├── services/ # Auth (Argon2id, JWT, TOTP encryption, TOTP replay helpers)
│ ├── static/js/ # Client-side crypto + vault UI │ ├── static/js/ # Client-side crypto + vault UI
│ └── templates/ # Jinja2 templates │ └── templates/ # Jinja2 templates
├── extension/ # Browser extension ├── extension/ # Browser extension
@@ -205,7 +216,7 @@ passkeeper/
│ ├── content/ # Content script (autofill, field detection) │ ├── content/ # Content script (autofill, field detection)
│ ├── shared/ # Shared crypto + Firefox polyfill │ ├── shared/ # Shared crypto + Firefox polyfill
│ ├── bridge/ # SSO bridge │ ├── bridge/ # SSO bridge
│ ├── background.js # Chrome MV3 service worker │ ├── background.js # Chrome MV3 service worker
│ ├── background.firefox.js # Firefox MV2 background page │ ├── background.firefox.js # Firefox MV2 background page
│ ├── manifest.json # Chrome/Edge MV3 │ ├── manifest.json # Chrome/Edge MV3
│ └── manifest.firefox.json # Firefox MV2 │ └── manifest.firefox.json # Firefox MV2
@@ -224,7 +235,7 @@ All vault/folder endpoints require `Authorization: Bearer <access_token>`.
|---|---|---| |---|---|---|
| POST | `/api/auth/register` | Create account | | POST | `/api/auth/register` | Create account |
| POST | `/api/auth/login` | Authenticate | | POST | `/api/auth/login` | Authenticate |
| POST | `/api/auth/mfa/verify` | Complete MFA | | POST | `/api/auth/mfa/verify` | Complete MFA (single-use code) |
| POST | `/api/auth/refresh` | Rotate tokens | | POST | `/api/auth/refresh` | Rotate tokens |
| POST | `/api/auth/logout` | Blacklist tokens | | POST | `/api/auth/logout` | Blacklist tokens |
| GET | `/api/vault` | List encrypted items | | GET | `/api/vault` | List encrypted items |
@@ -234,7 +245,7 @@ All vault/folder endpoints require `Authorization: Bearer <access_token>`.
| GET | `/api/vault/export` | Download encrypted JSON backup | | GET | `/api/vault/export` | Download encrypted JSON backup |
| POST | `/api/vault/import` | Bulk import; returns `{ imported, skipped }` | | POST | `/api/vault/import` | Bulk import; returns `{ imported, skipped }` |
| GET/POST | `/api/folders` | List / create folders | | GET/POST | `/api/folders` | List / create folders |
| POST | `/api/sharing` | Share item (ECDH re-encryption) | | POST | `/api/sharing` | Share item — sends `enc_name`/`iv_name` for ZK name |
| POST | `/api/emergency` | Create emergency access grant | | POST | `/api/emergency` | Create emergency access grant |
| POST | `/api/auth/change-password` | Atomic vault re-encryption | | POST | `/api/auth/change-password` | Atomic vault re-encryption |
| POST | `/api/auth/recover` | Account recovery (one-time) | | POST | `/api/auth/recover` | Account recovery (one-time) |
@@ -245,7 +256,7 @@ All vault/folder endpoints require `Authorization: Bearer <access_token>`.
Tags are stored as `plain.tags: string[]` inside the encrypted vault blob — the server never sees them and no schema change is required. 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. **Web app:** Add tags in the item edit modal (comma-separated). Tags appear as purple badge pills on item rows. A Tags section in the sidebar lets you filter by any tag. Search respects the active tag filter.
**Extension:** Tags appear as `.pk-tag` badges on item rows. Items tagged `favorite` appear in the **Favorites** tab and show a ★ in the site label. **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.