Aug 26 - Enhance security 4
CI / Python lint (flake8) (push) Has been cancelled
CI / Python syntax check (push) Has been cancelled
CI / Alembic migration chain (push) Has been cancelled
CI / JavaScript syntax check (push) Has been cancelled
CI / Pytest (push) Has been cancelled
CI / Build extension zip (push) Has been cancelled

This commit is contained in:
2026-08-26 14:19:25 -04:00
parent cc216b0d98
commit b84a6d9245
11 changed files with 546 additions and 33 deletions
+24 -1
View File
@@ -118,6 +118,8 @@ passkeeper/
│ ├── test_session_revocation.py # token_epoch revocation; deleted-account 401
│ ├── test_webauthn_uv.py # user verification required on both ceremonies
│ ├── test_sharing_expiry.py # expires_days fails closed
│ ├── test_registration_privacy.py # register does not disclose account existence
│ ├── test_emergency_visibility.py # grantor sees requests + retrievals
│ ├── test_deploy_config.py # nginx/gunicorn/systemd/extension packaging guards
│ └── js/test_psl.js # PSL same-site matching (node, run in CI)
├── gunicorn.conf.py # worker class, timeouts, preload_app=False
@@ -244,6 +246,7 @@ CREATE TABLE webauthn_credentials (
| `i9j0k1l2m3n4` | Add expires_at to shared_items |
| `j0k1l2m3n4o5` | Add recovery_verifier (decouple recovery proof) |
| `k1l2m3n4o5p6` | Add token_epoch (revoke sessions on pw change) |
| `l2m3n4o5p6q7` | Add emergency vault retrieval tracking |
---
@@ -301,6 +304,20 @@ CREATE TABLE webauthn_credentials (
possession of an unlocked device must not be sufficient.
- **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; recovery key derived with the user's email as a per-user PBKDF2 salt — legacy fixed salt `'passkeeper-recovery'` accepted transparently for codes created before this change
- **folder_id ownership:** validated server-side on all create/update/import operations — user cannot assign items to another user's folder
- **Registration privacy:** `POST /api/auth/register` returns an identical 202
whether or not the address exists, and performs an equivalent Argon2id hash on
both branches so timing does not reinstate the oracle. Duplicate attempts are
audited under `auth.register_duplicate`. Fully closing this needs email
verification so the address owner is told — until then the oracle is removed
but the owner cannot be notified.
- **Emergency access visibility:** `accept`, `request` and `vault_retrieved` are
audited under BOTH parties' user_ids. `/api/auth/audit-log` filters by
`user_id`, so an entry written only under the acting user is invisible to the
other — which meant a grantee could request and retrieve a vault snapshot
without anything reaching the grantor. `vault_retrieved_at` /
`vault_retrieval_count` on `emergency_access` record every fetch. Retrieval is
intentionally NOT blocked after the first time (the grantor may be unable to
re-provision); the wait period is the gate, and the grantor can revoke.
- **Audit logs:** never contain plaintext item names, shared item names, or vault data
---
@@ -642,6 +659,8 @@ Audit log details **never** contain plaintext item names, shared item names, or
- `password_changed_at` lives inside `plain` (encrypted) — never in the server schema
- WebAuthn `attachment`: `"cross-platform"` for security keys; `"platform"` for device biometrics (default)
- `enc_vault_is_legacy` check in `EmergencyAccess.to_dict()` is pure JSON inspection — no decryption
- Audit entries are only visible to the user_id they are written under — mirror cross-party events (use `_log_for_both` in `emergency.py`) or the other party never sees them
- `/register` must return the SAME body and status for new and existing addresses, and hash on both paths — returning early on duplicate reinstates a timing oracle
- Never return `str(e)` from exception handlers — log with `_log.exception(...)` and return a generic user-facing message to avoid leaking DB schema details or query fragments
- `extension/shared/psl.js` is GENERATED — never hand-edit; run `python scripts/update_psl.py`. It must load BEFORE content.js / popup.js / background.js in every manifest
- Never reintroduce `endsWith("." + host)` host matching anywhere in the extension — `tests/test_deploy_config.py` fails the build if it reappears
@@ -661,6 +680,7 @@ Audit log details **never** contain plaintext item names, shared item names, or
| Module | Action | Trigger |
| -------------- | ------------------------------------------------------ | ------------------------------ |
| `auth.py` | `auth.register` | New account |
| `auth.py` | `auth.register_duplicate` | Register attempt on an existing address (no email in detail) |
| `auth.py` | `auth.login` / `auth.login_failed` | Login success/fail |
| `auth.py` | `auth.account_locked` | Failed login lockout |
| `auth.py` | `auth.mfa_enable/disable/verify` | TOTP actions |
@@ -676,6 +696,7 @@ Audit log details **never** contain plaintext item names, shared item names, or
| `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 |
| `emergency.py` | `emergency_access.accept/request/vault_retrieved` | Logged under BOTH grantor and grantee user_ids |
| `webauthn.py` | `webauthn.register` | Passkey registered |
| `webauthn.py` | `webauthn.auth_success` / `webauthn.auth_failed` | Passkey login attempt |
| `webauthn.py` | `webauthn.rename` / `webauthn.delete` | Credential management |
@@ -857,7 +878,9 @@ Features planned for future implementation. Ordered by priority within each cate
- Autofill matching moved onto the Public Suffix List (registrable domains)
- Share `expires_days` fails closed instead of silently meaning "never"
- nginx `api_limit` corrected from 60r/m to 10r/s
- pytest suite (51 tests) + PSL node test + CI jobs; `gunicorn.conf.py`;
- Registration no longer discloses account existence (status, body and timing)
- Emergency access: requests and vault retrievals are now visible to the grantor
- pytest suite (66 tests) + PSL node test + CI jobs; `gunicorn.conf.py`;
systemd watchdog removed
### High priority — user-facing