05/18 Enhanced codes and functionalities 7

This commit is contained in:
2026-05-18 19:16:45 -04:00
parent c9808edde2
commit b9e8f5c357
5 changed files with 354 additions and 14 deletions
+92 -2
View File
@@ -52,13 +52,15 @@ passkeeper/
│ │ ├── shared_item.py # ECDH-encrypted cross-user shares; enc_name/iv_name
│ │ ├── emergency_access.py # State machine
│ │ ├── recovery_challenge.py # Server-side recovery challenge (multi-worker safe)
│ │ ├── webauthn_credential.py # Passkey / WebAuthn credentials (one row per key)
│ │ └── 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
│ │ ── emergency.py
│ │ └── webauthn.py # Passkey registration, authentication, credential management
│ ├── services/
│ │ └── auth_service.py # Argon2id, JWT, blacklist, @require_jwt, TOTP encrypt/decrypt,
│ │ # TOTP replay helpers (is_totp_code_used / mark_totp_code_used)
@@ -101,7 +103,8 @@ passkeeper/
│ ├── 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
── g7h8i9j0k1l2_encrypt_shared_item_name.py # enc_name/iv_name on shared_items
│ └── h8i9j0k1l2m3_add_webauthn_credentials_table.py # Passkey / WebAuthn credentials
├── scripts/
│ ├── reencrypt_totp_secrets.py
│ ├── backup_db.sh / backup.cron / passkeeper-logrotate
@@ -190,6 +193,21 @@ CREATE TABLE recovery_challenges (
);
-- (folders, token_blacklist, emergency_access, audit_logs — standard schemas)
-- WebAuthn / Passkey Credentials
CREATE TABLE webauthn_credentials (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL,
credential_id VARCHAR(512) NOT NULL UNIQUE, -- base64url authenticator credential ID
public_key TEXT NOT NULL, -- COSE public key, base64url
sign_count BIGINT NOT NULL DEFAULT 0, -- clone detection counter
transports VARCHAR(255), -- JSON list e.g. '["internal","hybrid"]'
aaguid VARCHAR(64), -- authenticator AAGUID
name VARCHAR(128) NOT NULL DEFAULT 'Passkey', -- user-assigned label
created_at DATETIME NOT NULL,
last_used_at DATETIME,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
```
---
@@ -206,6 +224,7 @@ CREATE TABLE recovery_challenges (
| `e5f6a7b8c9d0` | Add recovery_challenges table |
| `f6a7b8c9d0e1` | Add totp_used_codes table (TOTP replay prevent.) |
| `g7h8i9j0k1l2` | Add enc_name/iv_name to shared_items |
| `h8i9j0k1l2m3` | Add webauthn_credentials table (passkeys) |
---
@@ -221,6 +240,7 @@ CREATE TABLE recovery_challenges (
- **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
- **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
- **Password generator:** fully CSPRNG (`_cryptoRandInt` rejection-sampling)
- **Decrypted vault data:** `chrome.storage.session` only — never to disk
@@ -252,6 +272,64 @@ Requires Chrome 111+. `content.js` `onChanged` listener watches `area === 'sessi
## Key Implementation Details
### WebAuthn / Passkey
**Library:** `py-webauthn` (`webauthn>=2.0`). Installed via `pip install webauthn`.
**Config** (`.env` / environment):
```
WEBAUTHN_RP_ID=pwkeeper.ngodanguyen.tech # effective domain, no scheme/port
WEBAUTHN_RP_NAME=PassKeeper
WEBAUTHN_ORIGINS=https://pwkeeper.ngodanguyen.tech # comma-separated in env
```
In development: `WEBAUTHN_RP_ID=localhost`, `WEBAUTHN_ORIGINS=http://localhost:5000`.
**Flows:**
*Registration* (requires active JWT — user must be logged in):
```
POST /api/webauthn/register/begin → PublicKeyCredentialCreationOptions
POST /api/webauthn/register/complete → verifies attestation, stores credential
```
*Authentication* (unauthenticated — replaces password login):
```
POST /api/webauthn/authenticate/begin → PublicKeyCredentialRequestOptions
POST /api/webauthn/authenticate/complete → verifies assertion → { access_token, refresh_token, enc_key_salt }
```
Client still prompts for master password after successful assertion to derive vault key.
*Management* (requires JWT):
```
GET /api/webauthn/credentials → list registered passkeys
PATCH /api/webauthn/credentials/<id> → rename a passkey
DELETE /api/webauthn/credentials/<id> → remove a passkey
```
**Challenge storage:** challenge bytes are stored in the Flask session (signed cookie, `SECRET_KEY`). No DB row needed. Challenge is consumed (`session.pop`) on the `/complete` call.
**Clone detection:** `sign_count` is read before verification and updated after. `py-webauthn` raises on a decreasing counter.
**ZK preserved:** a WebAuthn assertion authenticates the user to the *server* only. The vault key `PBKDF2(masterPassword, enc_key_salt, 600k)` is never sent to or derivable by the server. After a passkey login the client must still enter the master password to unlock the vault.
**Client-side (`PasskeyAuth` in `auth.js`):**
- `loginWithPasskey(email)` — begin → `navigator.credentials.get()` → complete → stores tokens → redirects to `/vault`
- `registerPasskey(name)` — begin → `navigator.credentials.create()` → complete
- `_bufToB64url` / `_b64urlToBuf` — ArrayBuffer ↔ base64url conversion helpers
- `_credentialToJson(cred)` — serialises `PublicKeyCredential` for the server
**Settings UI (`vault.js` `loadPasskeys()`):** renders registered passkeys list with rename/delete; wires "Add passkey" button; hides the section if `window.PublicKeyCredential` is absent.
**`auth.js` login wiring:** "Sign in with Passkey" button on `login.html` calls `PasskeyAuth.loginWithPasskey(email)`. On success, stores tokens and navigates to `/vault` — unlock overlay fires if master password field was empty.
**Authenticator attachment:** currently `PLATFORM` (biometrics / device passkey). To support roaming authenticators (YubiKey, phone-as-key), remove or change `authenticator_attachment` in `register_begin`.
**Exception names** (`webauthn>=2.0`):
- `InvalidRegistrationResponse` — use in `register_complete`
- `InvalidAuthenticationResponse` — use in `authenticate_complete`
- `InvalidCBORData` — malformed CBOR attestation
- NOT `InvalidAuthenticatorResponse` (does not exist)
### 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`.
@@ -433,6 +511,9 @@ 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 |
| `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 |
---
@@ -466,6 +547,14 @@ GET|POST /api/emergency
DELETE /api/emergency/<id>
POST /api/emergency/<id>/accept|provide|request|deny
GET /api/emergency/<id>/vault
POST /api/webauthn/register/begin # start passkey registration (JWT required)
POST /api/webauthn/register/complete # finish registration + store credential
POST /api/webauthn/authenticate/begin # start passkey login (unauthenticated)
POST /api/webauthn/authenticate/complete # verify assertion → tokens + enc_key_salt
GET /api/webauthn/credentials # list registered passkeys (JWT required)
PATCH /api/webauthn/credentials/<id> # rename a passkey
DELETE /api/webauthn/credentials/<id> # remove a passkey
```
---
@@ -497,6 +586,7 @@ 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)
webauthn>=2.0 # Passkey / WebAuthn (py-webauthn)
```
---