Chat Encryption Implementation
Chat Encryption Implementation Field-level encryption details for chat data. All message encryption/decryption happens client-side; the server stores only en...
Chat Encryption Implementation
Field-level encryption details for chat data. All message encryption/decryption happens client-side; the server stores only encrypted blobs.
Summary
- Chat storage surfaces accept client-encrypted payloads, not Vault/server ciphertext.
- The backend can process plaintext for immediate AI inference, but persistent chat history must be written back as client-encrypted data.
- Existing persistence tests enforce the storage boundary that protects Directus and sync cache from server-side ciphertext formats.
Why This Exists
- Documents which fields are encrypted, with which keys, and at which layer
- Provides the implementation reference for the zero-knowledge architecture described in encryption-architecture.md
- Tracks the dual-phase processing model: plaintext for AI inference, encrypted for storage
How It Works
Encryption by Field
Client-Side Encrypted (Chat-Specific Key)
These fields are encrypted with a per-chat AES-256-GCM key. The chat key itself is wrapped with the user’s master key and stored as encrypted_chat_key.
| Field | Collection | Encrypted with |
|---|---|---|
encrypted_content |
messages | Chat key |
encrypted_sender_name |
messages | Chat key |
encrypted_category |
messages | Chat key |
encrypted_active_focus_id |
chats | Chat key |
encrypted_chat_summary |
chats | Chat key |
encrypted_chat_tags |
chats | Chat key |
encrypted_follow_up_request_suggestions |
chats | Chat key |
encrypted_chat_key |
chats | Master key |
Client-Side Encrypted (Master Key)
| Field | Collection | Notes |
|---|---|---|
encrypted_title |
chats | Chat title |
encrypted_draft_md |
drafts | Draft markdown content |
encrypted_draft_preview |
drafts | Draft preview text |
Server-Side Encrypted (Vault)
These require server access for processing:
| Field | Collection | Why server-side |
|---|---|---|
encrypted_credit_balance |
users | Billing validation |
encrypted_tfa_secret |
users | 2FA verification |
encrypted_username |
users |
Plaintext (Needed for Indexing/Sorting)
chat_id, hashed_user_id, messages_version, title_version, timestamps (created_at, updated_at, last_edited_overall_timestamp, last_message_timestamp), unread_count, message_id, client_message_id, role.
Chat Key Management
- Client generates 32-byte AES key per chat via
generateChatKey()in cryptoService.ts - Key wrapped with master key via
encryptChatKeyWithMasterKey(), stored aschats.encrypted_chat_key - On new device login: all encrypted chat keys downloaded and decrypted with master key
- Embed keys derived deterministically from chat key via HKDF:
deriveEmbedKeyFromChatKey(chatKey, embedId)
Chat Key Immutability Guard
Once a chat has an encrypted_chat_key, the server blocks overwrites unless allow_chat_key_rotation = true is set (used for hidden-chat hide/unhide). This guard operates at two levels:
- WebSocket handler: encrypted_chat_metadata_handler.py compares incoming key fingerprint against cached key
- Persistence task: persistence_tasks.py checks Directus before writing
Dual-Phase Processing Model
Phase 1 – AI Processing: Client sends plaintext content for immediate LLM processing. Server generates metadata (title, category) during preprocessing, streams AI response as markdown.
Phase 2 – Encrypted Storage: Server sends generated metadata back to client. Client encrypts everything (user message, AI response, title, category) with appropriate keys. Client sends encrypted package to server for permanent storage in Directus.
Critical rule: server never stores Tiptap JSON (only exists client-side). All server storage is markdown-based.
Server-Side Encryption Scope
encryption.py no longer handles chat or draft encryption – those methods were removed for zero-knowledge. It retains:
- User-specific Vault key management (
create_user_key,encrypt_with_user_key,decrypt_with_user_key) - Email HMAC hashing
- System-level encryption keys (newsletter, issue reports, debug requests, demo chats, creator income, support payments)
Data Structures
Chat Schema Additions
In chats.yml:
encrypted_chat_summary(text) – 2-3 sentence summary, chat-key encryptedencrypted_chat_tags(json) – up to 10 tags, chat-key encryptedencrypted_follow_up_request_suggestions(json) – 6 suggestions, chat-key encryptedencrypted_chat_key(string, max 512) – per-chat AES key wrapped with master key
Message Schema Additions
In messages.yml:
encrypted_sender_name(string) – chat-key encryptedencrypted_category(string) – chat-key encrypted (replaced former plaintextcategory)
Edge Cases
- Multi-device sync: encrypted chat keys are synced via Directus; each device decrypts with master key
- Key corruption prevention: immutability guard ensures a misconfigured device cannot overwrite a chat key and make existing messages undecryptable
- Array encryption:
encryptArrayWithChatKey()/decryptArrayWithChatKey()handle JSON array fields (tags, suggestions) - Backward compatibility: frontend handles both encrypted and legacy plaintext data during transition
Related Docs
- Encryption Architecture – encryption tiers and master key lifecycle
- Security Architecture – overall security model
- Message Processing – dual-cache (vault vs client encryption)