Encrypt uploaded files at rest, like note bodies already were
Note text was encrypted from the start; the files beside it were not. A copy of the uploads directory handed over every image in the clear while the notes referring to them stayed unreadable — half a protection, and the half that is easiest to walk off with. Same key and cipher as note content, so a backup is worth exactly as much as its .env either way, which was already true and is now true uniformly. Files carry an INKENC01 header and say for themselves whether they are encrypted. That is what makes the startup sweep idempotent, and it had to be the file rather than a database row: attachments are shared between users by content hash, so no single row owns one. Named by the hash of the plaintext, so identical uploads still collapse into one copy — a fresh IV per file would have defeated deduplication. Read whole rather than streamed, because GCM only proves integrity at the last block. Written through a temporary name and renamed, so a crash cannot strand a half-encrypted file that would fail its tag forever. Verified: the one existing attachment went from 78 to 114 bytes with the header in front, a second start did nothing, and images still load in the browser after a reload. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
df279a5d36
commit
915856e1a4
2 files changed
+129
-4
No files matched your search
@@ -24,7 +24,7 @@ A self-hosted note-taking application with rich text editing, freehand drawing,
|
||||
- **Offline-First** — Works fully offline, syncs when connection returns
|
||||
- **Conflict-Aware Sync** — A write from a device that missed another's changes keeps the state it replaces as a version, rather than burying it
|
||||
- **WebSocket Real-Time Push** — Changes appear on other devices instantly
|
||||
- **Encrypted at Rest** — AES-256-CBC encryption for all note content on the server
|
||||
- **Encrypted at Rest** — AES-256-GCM for note content *and* uploaded files on the server
|
||||
- **JWT Authentication** — Username + password accounts with bcrypt password hashing. No public
|
||||
sign-up: an administrator creates the accounts
|
||||
- **Vector Drawings** — Strokes are stored as vectors in one canonical page space, so a drawing looks identical on every screen and stays sharp at any pixel density
|
||||
@@ -409,6 +409,32 @@ a snippet around the first match.
|
||||
|
||||
---
|
||||
|
||||
## Encryption at rest
|
||||
|
||||
Note bodies and uploaded files are both encrypted with AES-256-GCM under
|
||||
`ENCRYPTION_KEY`. Losing that key loses the notes — back it up with the database,
|
||||
not next to it.
|
||||
|
||||
Files carry an 8-byte `INKENC01` header, so a file says for itself whether it is
|
||||
encrypted. That is what lets the startup sweep run on every boot and encrypt only
|
||||
what is still in the clear: files are shared between users by content hash, so a
|
||||
flag on a database row could not have answered the question.
|
||||
|
||||
Two consequences worth knowing:
|
||||
|
||||
- **Deduplication still works.** Files are named by the hash of the *plaintext*,
|
||||
so two identical uploads share one copy — encrypting each with a fresh IV would
|
||||
otherwise produce two different files from the same picture.
|
||||
- **Reads are not streamed.** GCM only proves a file is intact once the final
|
||||
block is in, so an attachment is read whole and verified before anything is
|
||||
sent. Half of a corrupt image is worse than an error.
|
||||
|
||||
Uploads are written to a temporary name and renamed into place. A rename is
|
||||
atomic on one filesystem, so a crash mid-write leaves the old file or the new
|
||||
one, never a half-written one that would fail its tag forever after.
|
||||
|
||||
---
|
||||
|
||||
## Attachments
|
||||
|
||||
Images used to be inlined into the note body as base64 data URIs — a few
|
||||
|
||||
Reference in new issue
Block a user