From 78334982e00c9c44843a9a970f39445c7b429f18 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Fri, 22 May 2026 11:00:30 -0400 Subject: [PATCH] 05/22 Update documents --- CLAUDE.md | 31 ++++++++++++++++++++++++++----- README.md | 5 +++-- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7aa1953..cb7bb89 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -246,7 +246,7 @@ 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 -- **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 +- **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 @@ -404,7 +404,7 @@ Every accepted TOTP code is recorded in `totp_used_codes` (user_id + code, 120s ### 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. +`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 verification callsites — both `login` and `change_password`. ### folder_id ownership validation @@ -416,13 +416,32 @@ Every accepted TOTP code is recorded in `totp_used_codes` (user_id + code, 120s 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 + Recovery key = PBKDF2(recoveryCode, userEmail, 200k iter) + Legacy fallback: PBKDF2(recoveryCode, "passkeeper-recovery", 200k iter) 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) + Returns { id, enc_data, iv, enc_name, iv_name } per item 5. POST /recover → validates proof again; consumes rotated challenge; atomically - re-encrypts vault + resets password + re-encrypts vault (enc_data + enc_name) + resets password ``` +### Password change key rotation + +`handleChangePassword` in `vault.js` performs a fully atomic key rotation in a single `POST /api/auth/change-password` call: + +1. Derives `newVaultKey = PBKDF2(newPassword, newEncKeySalt, 600k iter)` +2. Re-encrypts every vault item's `enc_data` + `enc_name` with `newVaultKey` +3. Re-encrypts the ECDH sharing private key at the raw bytes level: + - Fetches `GET /api/sharing/keys` → gets `private_key_enc` / `private_key_iv` + - `AES-GCM.decrypt(vaultKey, ...)` → raw private key bytes + - `AES-GCM.encrypt(newVaultKey, newIv, ...)` → new ciphertext + - Sends `sharing_private_key_enc` / `sharing_private_key_iv` in the payload +4. Server atomically updates `master_hash`, `enc_key_salt`, all vault items, + and `sharing_private_key_enc`/`sharing_private_key_iv` in one transaction + +The sharing key uses raw `SubtleCrypto` calls (not `SharingCrypto.decryptPrivateKey`) because `decryptPrivateKey` imports the key with `extractable: false`, making it impossible to re-export the bytes for re-encryption. + ### 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). @@ -555,11 +574,13 @@ Audit log details **never** contain plaintext item names, shared item names, or - 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 - `_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` +- `verify_auth_token` must receive `user=user` at both `login` and `change_password` to enable Argon2 rehash +- APScheduler cleanup job handles `TokenBlacklist`, `RecoveryChallenge`, AND `TotpUsedCode`; guard with `os.environ.get('WERKZEUG_RUN_MAIN') == 'true'` in Flask debug mode to prevent double-start - `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 +- 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 +- `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) --- diff --git a/README.md b/README.md index a1af6b3..0644b14 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,8 @@ A self-hosted, zero-knowledge password manager — web app and Chrome/Firefox ex - **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); single-use code enforcement prevents replay attacks - **Passkeys / WebAuthn** — register device biometrics or hardware keys (YubiKey, cross-device QR) as a sign-in method; master password still required to unlock vault (zero-knowledge preserved); manage passkeys in Account Settings; transport type shown per credential (📱 Device / 🔑 Security key) -- **Master password change** — atomic zero-knowledge re-encryption of entire vault including item names -- **Account recovery** — 128-bit recovery code; server never stores it; challenge-response proof prevents forgery +- **Master password change** — atomic zero-knowledge re-encryption of entire vault including item names and ECDH sharing private key; sharing continues to work after password change +- **Account recovery** — 128-bit recovery code; server never stores it; challenge-response proof prevents forgery; vault item names re-encrypted along with vault data; recovery key uses per-user email as PBKDF2 salt - **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 - **Browser history** — back/forward button works for all views (`history.pushState`) @@ -81,6 +81,7 @@ Sharing: ECDH(Alice_priv, Bob_pub) ──► sharedKey ──► AES-256-GCM(en - **Passkey / WebAuthn** — FIDO2 assertion proves identity to the server without a password; vault key still derived from master password client-side; `sign_count` updated on each use for clone detection - **Password age tracking** — `password_changed_at` stored inside encrypted blob; security dashboard uses actual password change date, not item creation date - **Emergency access stale snapshot detection** — server flags provisioned snapshots created before the `enc_name` fix; UI prompts grantor to re-provision +- **Recovery key hardening** — recovery key derived with `PBKDF2(recoveryCode, userEmail, 200k iter)`; per-user email salt prevents cross-account recovery code reuse; legacy `'passkeeper-recovery'` salt accepted transparently for existing codes A database breach exposes only encrypted ciphertext. The server cannot read vault names, passwords, tags, or shared item names.