-
-
Notifications
You must be signed in to change notification settings - Fork 0
Security Model
MoodHaven Journal is a zero-knowledge journaling app. All encryption and decryption happens client-side in the browser/WebView using the WebCrypto API. The Rust backend stores and retrieves opaque encrypted blobs — it never sees plaintext journal content.
For the general overview → README
- Keys derived from user password via PBKDF2 (600,000 iterations, random salt per entry) — never stored
- All encryption/decryption happens client-side; backend never sees plaintext
- No master keys, admin passwords, or cloud recovery mechanisms
- Only a salted hash is stored for password verification
- Password verification runs in Rust (
verify_passwordcommand) — the hash never leaves the backend
User Password
│
│ PBKDF2-HMAC-SHA-256 (600,000 iterations + per-entry random 16-byte salt)
▼
256-bit Encryption Key (never stored, lives in JS memory while unlocked)
│
├──▶ Journal entry text ──▶ AES-256-GCM ──▶ SQLite `content` column
│ (EncryptedContent: { iv: base64, data: base64, salt: base64 })
│
├──▶ Signals payload ──▶ AES-256-GCM ──▶ SQLite `payload` column
│
├──▶ Media file bytes ──▶ AES-256-GCM ──▶ {app_data}/media/ (encrypted file)
│
└──▶ Export file ──▶ AES-256-GCM ──▶ .moodhaven file
Each entry has its own random salt. Compromising one derived key exposes only that entry.
| Field | Reason |
|---|---|
mood (1–5) |
Required for local analytics without decrypting every entry |
created_at, updated_at
|
Required for calendar view, timeline ordering, and sync |
location_weather |
Opt-in; no journal content; required for weather chip |
| Tag names | Required for search index |
pinned flag |
Required for timeline ordering |
book_id |
Required for filtering |
- Password (+ optional 2FA) → derives encryption key, decrypts data
- Erase & Start Fresh → destroys all data, no password needed (the "forgot password" escape hatch)
- 24-character code (
XXXX-XXXX-XXXX-XXXX-XXXX-XXXX), shown once at setup - Encrypts a copy of the user's password (key escrow) — the ONLY remote recovery path
- Hardware keys do NOT bypass encryption; they are a second factor only
- Standard TOTP (RFC 6238), works with any authenticator app
- 6-digit backup codes (single-use, stored as hashed values)
- Disable path: password + TOTP → disable; or use a backup code
- Native Rust CTAP2/HID — NOT browser WebAuthn (doesn't work in Tauri WebView)
- Feature flag:
--features hardware-key - Linux runtime dep:
libudev1; build dep:libudev-dev - UI falls back to platform install instructions when feature absent or dependency missing
- Files:
src-tauri/src/commands/hardware_key.rs,src/lib/hardwareKeyService.ts
- All backups AES-256-GCM encrypted client-side before upload
- WebDAV server only ever receives ciphertext
- Uses
tauri-plugin-http(bypasses WebView CSP for user-configured URLs) - Sync is manual only — user explicitly triggers each upload/download
When AI features are enabled:
- Journal text never sent to any external service
- Only anonymised metadata sent: mood scores, entry frequency, time-of-day patterns, emotional categories (locally extracted)
- AI features are opt-in, disabled by default, require explicit consent
Commands that touch sensitive data require an active unlocked session. Calling them before unlock_app returns "Session is locked". Affected: analytics, settings (get_setting, set_setting, get_all_settings), Oura Ring, time capsule.
factory_reset explicitly does not require unlock — it is the forgot-password escape hatch.
Permitted in src-tauri/capabilities/default.json:
| Capability | Purpose |
|---|---|
core:default |
Standard commands |
shell:allow-open |
Open URLs in browser |
notification:default |
Reminders |
http:default |
WebDAV sync (bypasses CSP) |
- Sensitive data encrypted before storage
- No hardcoded secrets or keys
- User input sanitised (prevent injection)
- File paths validated (prevent path traversal)
- Error messages don't leak sensitive info
- Tauri commands use proper permission scopes
eval(userInput) // code injection
fs.readFile(userProvidedPath) // path traversal
localStorage.setItem('key', password) // unencrypted secrets
console.log(userData) // sensitive data in logs-
SECURITY.md— vulnerability reporting policy -
src/lib/services/crypto.ts— client-side crypto implementation -
.claude/docs/security.md— security model and checklist