eleven / messenger Public

docs: scope the "server compromise" claim to message content (#100)

The README claimed a server compromise "reveals nothing." That's now accurate for
a passive/stolen-database attacker's access to message *content* (strengthened by
the token-hashing and invite-claim fixes), but it overstated two structural
realities: the server sees metadata (screen names + the membership graph, which it
needs in order to route), and public keys are distributed by the server with no
out-of-band verification, so an *active* malicious server could substitute a key
and MITM new conversations.

Scope the headline and CLI-section wording to "no message content" and add two
Trade-offs bullets (unencrypted metadata; server-trusted key distribution).

Considered but rejected: encrypting roster labels under the instance key — it would
blind the admin console (which holds no member key by design) and wouldn't hide the
membership graph anyway (the server routes by it).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 8281d2152cb9c32ae122117c4151ce60848bf223 parent 79e6bba
1 file changed, +17 −5
  • README.md +17 −5
diff --git a/README.md b/README.md
index f6e27c2..5941fde 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,9 @@
A tiny, kid-friendly chat — DMs and group chats, no logins, no passwords, just
secret invite links. Messages are **end-to-end encrypted**: the server only ever
-sees ciphertext, so compromising the server reveals nothing.
+sees ciphertext, so a stolen database or server snapshot exposes no message
+content. (It does see metadata — screen names and who shares a thread with whom;
+see Trade-offs.)
## How the encryption works
@@ -17,8 +19,7 @@ sees ciphertext, so compromising the server reveals nothing.
- An invite link carries a one-time secret in its `#fragment` (e.g.
`/join/<name>#<secret>`) used to bootstrap the joiner's keys — never sent to the
server. The **admin token authorises roster management only; it is not a
- decryption key.** Even with the whole database and server, an attacker can't read
- messages.
+ decryption key.** Even a full copy of the database reveals no message content.
**Trade-offs (inherent to this model):**
@@ -28,6 +29,17 @@ sees ciphertext, so compromising the server reveals nothing.
claw back a key they already hold (no forward secrecy on leave).
- If a member loses their device with no backup/add-device link, their key
material — and the threads only they could decrypt — is gone.
+- **Metadata is not encrypted.** The server can't read message content, but the
+ roster stores each member's screen name in clear (separate from the encrypted
+ copy carried inside each message), and the server necessarily knows who shares
+ a thread with whom in order to route messages. So a server compromise reveals
+ the social graph and names — just not what anyone said.
+- **Key distribution trusts the server.** Public keys are handed out by the
+ server's roster and aren't yet verified out of band (no safety-number check).
+ A *passive* compromise — a stolen database — can't use that (the content
+ guarantee above holds), but an *active* malicious server could substitute a
+ public key to intercept *new* conversations. Out-of-band key verification is
+ planned to close this.
## Run it
@@ -211,8 +223,8 @@ Under the hood it `POST`s to `/api/messages?token=…` with
`{"thread_id":"…","blob":"<ciphertext>"}`. The server checks the poster is a
member of that thread, then stores, broadcasts, and pushes it just like a
WebSocket `msg` frame — **it never sees the plaintext, the private key, or the
-thread key.** Keep those on the calling host, never on the server, and the
-"compromise the server, get nothing" guarantee holds.
+thread key.** Keep those on the calling host, never on the server, so the server
+holds only ciphertext it can't open (the message-content guarantee above).
## Tests