Aug 26 - Enhance security 2
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 12:54:17 -04:00
parent 82dd7c5aef
commit 6c1bef73c8
20 changed files with 1193 additions and 79 deletions
+58 -2
View File
@@ -109,6 +109,16 @@ passkeeper/
│ ├── reencrypt_totp_secrets.py
│ ├── backup_db.sh / backup.cron / passkeeper-logrotate
│ ├── passkeeper-nginx.conf / passkeeper.service
├── tests/ # pytest suite — in-memory SQLite, no MySQL needed
│ ├── conftest.py # app/client fixtures + register/login helpers
│ ├── test_mfa_gate.py # enc_key_salt withheld until MFA; proof not forgeable
│ ├── test_key_rotation.py # re-encryption completeness guard
│ ├── test_session_revocation.py # token_epoch revocation; deleted-account 401
│ ├── test_webauthn_uv.py # user verification required on both ceremonies
│ └── test_deploy_config.py # nginx/gunicorn/systemd invariants (502 guards)
├── gunicorn.conf.py # worker class, timeouts, preload_app=False
├── pytest.ini
├── requirements-dev.txt
├── reset_db.py
├── requirements.txt
├── wsgi.py / run.py
@@ -135,7 +145,9 @@ CREATE TABLE users (
sharing_private_key_enc TEXT,
sharing_private_key_iv VARCHAR(64),
recovery_enc_salt VARCHAR(128),
recovery_iv VARCHAR(64)
recovery_iv VARCHAR(64),
recovery_verifier VARCHAR(64), -- HMAC key for the recovery challenge
token_epoch INT NOT NULL DEFAULT 0 -- session generation counter
);
-- Vault Items
@@ -225,6 +237,9 @@ CREATE TABLE webauthn_credentials (
| `f6a7b8c9d0e1` | Add totp_used_codes table (TOTP replay prevent.) |
| `g7h8i9j0k1l2` | Add enc_name/iv_name to shared_items |
| `h8i9j0k1l2m3` | Add webauthn_credentials table (passkeys) |
| `i9j0k1l2m3n4` | Add expires_at to shared_items |
| `j0k1l2m3n4o5` | Add recovery_verifier (decouple recovery proof) |
| `k1l2m3n4o5p6` | Add token_epoch (revoke sessions on pw change) |
---
@@ -238,7 +253,14 @@ CREATE TABLE webauthn_credentials (
- **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
- **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.
Every token carries an `epoch` claim checked against `users.token_epoch`;
changing the master password or recovering the account increments it, which
revokes every outstanding access AND refresh token. Tokens minted before the
claim existed decode as `epoch 0` and remain valid until the next change.
- **MFA gate:** `enc_key_salt` is NOT returned by `/login` when TOTP is enabled —
it is released by `/mfa/verify` once both factors are proven. Returning it
early let a password-only attacker forge a recovery proof (see below).
- **MFA:** TOTP secret AES-256-GCM encrypted at rest; each code is single-use (replay prevented via `totp_used_codes` table, 120s TTL)
- **Passkeys / WebAuthn:** server authentication via FIDO2; ZK model preserved — WebAuthn proves identity to the server but the vault key is still derived from the master password client-side; `sign_count` updated on every assertion for clone detection
- **Sharing:** ECDH P-256 zero-knowledge re-encryption; item name also encrypted with shared key
@@ -246,6 +268,24 @@ CREATE TABLE webauthn_credentials (
- **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
- **Recovery proof key:** `HMAC-SHA256(key=recovery_verifier, msg=nonce)`.
`recovery_verifier` = `PBKDF2(recoveryCode, "passkeeper-recovery-verifier:" + email, 200k)`,
derived client-side from the recovery code alone and used for nothing else.
It must NEVER be `enc_key_salt`: that value doubles as the vault-key PBKDF2
salt and is disclosed to the client at login, so keying the proof with it
allowed anyone holding the master password to pull the entire vault from the
unauthenticated `/recovery/items` — bypassing MFA. Accounts whose recovery
code predates the column fall back to the legacy key and are flagged via
`recovery_is_legacy` on `/recovery/status`.
- **Key rotation completeness:** `change_password` and `/recover` refuse (409
`incomplete_reencryption`) unless the client's `items` payload covers every
vault item the user owns — a short payload would rotate `enc_key_salt` and
leave the missing items permanently undecryptable. `/recover` accepts
`allow_partial: true` after the user confirms the loss (otherwise one corrupt
item locks them out forever); `change_password` has no such override.
- **Passkeys:** both ceremonies use `UserVerificationRequirement.REQUIRED` and
`require_user_verification=True`. A passkey replaces password AND TOTP, so
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
- **Audit logs:** never contain plaintext item names, shared item names, or vault data
@@ -590,6 +630,12 @@ Audit log details **never** contain plaintext item names, shared item names, or
- 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
- 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
- `preload_app` must stay `False` in `gunicorn.conf.py` — APScheduler's thread does not survive `fork()`, so `--preload` silently disables the cleanup job
- nginx `proxy_read_timeout` must stay BELOW gunicorn `timeout`, else a slow request returns 502 instead of 504
- `WatchdogSec` in the systemd unit requires `Type=notify` + `NotifyAccess=main`; without them systemd SIGKILLs the service on a loop
- `ExecReload` must not use `USR2` — it forks a second master and strands `$MAINPID`
- `ProductionConfig.validate()` is called from `create_app()`, NOT at class-definition time — running it in the class body made `import app.config` fail without production secrets, breaking tests and local tooling
- Always pass `user.token_epoch` to `generate_tokens()` — the `0` default exists only so stale call sites fail visibly
- `SharingCrypto.decryptPrivateKey` imports the key with `extractable: false` — use raw `SubtleCrypto` calls when you need the key bytes (e.g. re-encryption on password change)
---
@@ -785,6 +831,15 @@ page itself.
Features planned for future implementation. Ordered by priority within each category.
### Recently completed (Aug 2026)
- MFA bypass closed: `enc_key_salt` withheld until the second factor; recovery
proof rebased onto `recovery_verifier`
- Key-rotation completeness guard on `change_password` / `/recover`
- Session revocation via `token_epoch`; `require_jwt` now verifies the user exists
- Passkey ceremonies require user verification
- pytest suite (37 tests) + CI job; `gunicorn.conf.py`; systemd watchdog removed
### High priority — user-facing
**1. One-time share links**
@@ -872,6 +927,7 @@ Extend `backup.cron` to run a weekly restore test into a throwaway DB, verifying
2. `syntax-check``ast.parse` all `app/` Python files
3. `migration-check` — single Alembic head, no duplicate revision IDs
4. `js-syntax``node -e "new Function(...)"` on all vault/extension JS files
4b. `tests` — pytest suite against in-memory SQLite (`TestingConfig`); gates `build-extension`
5. `build-extension` — produces `passkeeper-extension-chrome.zip` and `passkeeper-extension-firefox.zip` as artifacts (30-day retention)
**Runner:** self-hosted host-mode runner on the production server. Requires `python3`, `pip3`, `node`, `zip` on the host. No Docker needed.