Signup & Login
Signup & Login Summary Login requests are keyed by and , not plaintext email or password. Supported login methods all converge on the same session response s...
Signup & Login
Summary
- Login requests are keyed by
hashed_emailandlookup_hash, not plaintext email or password. - Supported login methods all converge on the same session response shape and client-origin verification boundary.
- Authenticated session checks must fall back from cache to Directus before failing, so cache misses do not log users out by themselves.
Zero-knowledge password verification: the server verifies you know your password without ever learning it, via a pre-computed lookup hash. Emails are looked up by a SHA-256 hash, and master encryption keys are derived on the device and wrapped before transmission. Authentication is proven by successful client-side decryption.
Note on terminology: “zero-knowledge” here refers specifically to password verification and key derivation — it does NOT mean the server cannot decrypt your stored content. The server can decrypt
encrypted_*columns transiently in memory when running an AI response, rendering an invoice, or delivering a reminder; it just never persists plaintext. See encryption-architecture.md for the full posture.
Why This Exists
- The server must authenticate users without ever seeing their password or email in plaintext
- Multiple login methods (password+2FA, passkey, recovery key) each need their own path to the same master key
- Session persistence must work reliably across Safari iOS/iPadOS strict cookie policies
Implementation Status
- Implemented: email + password + OTP 2FA, passkey (PRF), recovery key login
- Planned: magic link login, API key access
How It Works
graph TB
subgraph "Signup"
S1[Email + username] --> S2[Confirm email via OTP]
S2 --> S3{Choose auth method}
S3 -->|Password| S4["PBKDF2(password, salt)<br/>→ wrap master key"]
S3 -->|Passkey| S5["PRF + HKDF<br/>→ wrap master key"]
S4 --> S6[Mandatory 2FA setup]
S6 --> S7[Recovery key generated<br/>+ mandatory download]
S5 --> S7
end
subgraph "Login Paths → Master Key"
L1[Password + 2FA] --> L5["PBKDF2 → unwrap"]
L2[Passkey + PRF] --> L5
L3[Recovery Key] --> L5
L5 --> L6[Master key in memory<br/>→ decrypt all user data]
end
Signup Flow
Step 1 – Basics: user provides username and email; server checks email uniqueness via hashed_email.
Step 2 – Confirm email: 6-digit OTP sent to email, validated server-side.
Step 3 – Secure account: user chooses password or passkey.
Password path:
- Client generates master key via
generateExtractableMasterKey()and a unique salt - Wrapping key derived via
deriveKeyFromPassword()– PBKDF2-SHA256, 100k iterations - Master key wrapped with wrapping key; only
hashed_email,encrypted_email, salt, and wrapped master key sent to server - Plaintext password never transmitted
Passkey path:
- Server generates WebAuthn registration options with PRF extension
- Browser creates credential; client verifies PRF support and derives PRF output from creation or an immediate scoped assertion
- Client generates master key, wraps it with
deriveWrappingKeyFromPRF()(HKDF from PRF signature + user salt) - Wrapped master key uploaded to server
See Passkeys for full passkey details.
Step 3.2 – 2FA setup (password users only): QR code scanned with authenticator app, 6-digit OTP verified. Password auth always requires 2FA – they are set up together and cannot exist independently.
Step 4 – Recovery key: mandatory. Auto-generated, auto-downloaded as openmates_recovery_key.txt. User must confirm storage before proceeding. See RecoveryKeyTopContent.svelte.
Step 5 – Profile image: (work in progress)
Login Flows
Password + 2FA Login
- Client computes
hashed_emailfor lookup - Server returns user’s salt and wrapped master key
- Client derives wrapping key via PBKDF2(password, salt) and unwraps master key
- User enters OTP (or backup code); server verifies
- If verified: server sets session cookies, client decrypts user data with master key
Passkey Login
- User clicks “Login with passkey” (no email entry needed)
POST /auth/passkey/assertion/initiatereturns WebAuthn challenge with PRF extension using global saltSHA256(rp_id)[:32]- Browser prompts for passkey authentication
POST /auth/passkey/assertion/verifyverifies signature viapy_webauthn, identifies user bycredential_id, starts cache warming- Client derives wrapping key from PRF via
HKDF(PRF_signature, user_email_salt), unwraps master key - Client decrypts email from
encrypted_email_with_master_key, deriveslookup_hash POST /auth/loginwithlookup_hashandlogin_method: 'passkey'completes session- Frontend waits for cache warming (via WebSocket sync status) before loading main interface
See auth_passkey.py and Login.svelte.
Recovery Key Login
Recovery key uses the same PBKDF2 derivation path as password login. Users who still have their recovery key use “Login with recovery key” on the login page – this preserves all data. See Account Recovery for the destructive reset flow when all credentials are lost.
Session Persistence (“Stay Logged In”)
| Setting | Cookie TTL | Master key storage | Cleanup |
|---|---|---|---|
| Unchecked (default) | 24 hours | Memory only (memoryMasterKey) |
Auto-cleared on page close |
| Checked | 30 days | IndexedDB as CryptoKey | Persists across sessions |
The 30-day TTL addresses Safari iOS strict cookie policies that cause logout on page reload.
Implementation: preference captured during email lookup, stored in Redis, cookie max_age set to 2,592,000s or 86,400s accordingly. See cryptoKeyStorage.ts for storage logic.
Validation layers: page-load check, access-time validation, periodic validation timer. Memory keys need no cleanup handlers (cleared automatically when page closes).
Edge Cases
- Safari iOS cookie eviction: 30-day TTL +
navigator.storage.persist()for IndexedDB - Lost password + lost passkey + lost recovery key: destructive account reset required (see Account Recovery)
- Non-PRF passkey device: signup blocked; user prompted to use password+2FA or switch to a PRF-capable manager
Related Docs
- Security Architecture – zero-knowledge password verification overview
- Client-Side Encryption – master key lifecycle
- Passkeys – WebAuthn PRF implementation
- Account Recovery – recovery and reset flows