Skip to content

Security Model

Kenneth LaCroix edited this page Apr 13, 2026 · 1 revision

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


Zero-Knowledge Architecture

  • 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_password command) — the hash never leaves the backend

Encryption Flow

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.

Stored Unencrypted (Intentional)

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

Two Unlock Paths

  1. Password (+ optional 2FA) → derives encryption key, decrypts data
  2. Erase & Start Fresh → destroys all data, no password needed (the "forgot password" escape hatch)

Recovery Key

  • 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

Two-Factor Authentication

TOTP

  • 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

Hardware Key (FIDO2 / YubiKey)

  • 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

Cloud Sync Security

  • 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

AI Privacy

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

Session Lock Guards

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.

Tauri Capabilities

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)

Security Checklist for New Features

  • 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

Forbidden Patterns

eval(userInput)                          // code injection
fs.readFile(userProvidedPath)            // path traversal
localStorage.setItem('key', password)    // unencrypted secrets
console.log(userData)                    // sensitive data in logs

Full Reference

Clone this wiki locally