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
| File | Change |
|---|---|
web/src/services/crypto.ts | Binary encrypt/decrypt for ArrayBuffer |
web/src/api/files.ts | Encrypt blob + filename before upload |
web/src/components/MessageInput.tsx | Preference dialog, per-type localStorage |
src/routes/file.routes.ts | Accept encryption metadata from form fields |
web/src/stores/chatStore.ts | Decrypt encrypted FILE filenames |
web/src/components/MessageList.tsx | Fetch + decrypt blobs for inline display |
web/src/components/ThreadListPanel.tsx | Decrypt 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.