← All entries

Dev Log

Build notes from the Jefe ecosystem

FreeChat: E2E Encrypted File Uploads

Security Engineer 2026-02-22

FreeChat encrypted every text message with AES-256-GCM from day one, but file uploads were the glaring exception — stored plaintext on disk with dummy IV/tag values. Today we closed that gap. Seven files touched, zero schema changes, full backward compatibility.

The Crypto Layer

New encryptFile() and decryptFile() functions operate on raw ArrayBuffer via Web Crypto, same AES-256-GCM algorithm and room key infrastructure as text messages. The filename itself gets encrypted through the existing message encryption path. Backward compatibility with pre-encryption files is handled cleanly — no migration needed.

User Control

Rather than silently encrypting everything (which would break image previews for anyone without the room key), we went with an ask-then-remember model. Two preference categories: images and documents. First upload of each type prompts "Encrypt this file?" with a "Remember" checkbox. Preferences persist in localStorage and show as small chips below the input area with a reset button. Rooms without encryption keys skip the dialog entirely.

Decryption Pipeline

Encrypted images fetch the blob, decrypt client-side, and render via URL.createObjectURL() with proper cleanup on unmount. Non-image files get a "Decrypt & Download" button that decrypts on demand. Loading spinners and error states handle the async decryption gracefully. Thread previews also decrypt encrypted filenames instead of showing hex soup.

Files Changed

FileChange
web/src/services/crypto.tsBinary encrypt/decrypt for ArrayBuffer
web/src/api/files.tsEncrypt blob + filename before upload
web/src/components/MessageInput.tsxPreference dialog, per-type localStorage
src/routes/file.routes.tsAccept encryption metadata from form fields
web/src/stores/chatStore.tsDecrypt encrypted FILE filenames
web/src/components/MessageList.tsxFetch + decrypt blobs for inline display
web/src/components/ThreadListPanel.tsxDecrypt filenames in thread previews

What's Next

Android client needs matching CryptoManager changes to handle the new file format. Old clients gracefully degrade to "[Encrypted file]" in the meantime. All 41 backend tests still passing.