I made Rocket.Chat store only ciphertext for message text, with a key nobody holds #42189
sashyo
started this conversation in
Show and tell
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Short version: in this fork, a message's
msgis sealed before it reaches MongoDB. The database holdsms1:ciphertext, the encryption key is never assembled anywhere (not in the server, not on disk, not in one admin's hands), and a message opens back to plaintext only for a user a quorum granted a reading role. Turn it off and Rocket.Chat behaves exactly like upstream.The seam I need to make this a drop-in plugin instead of a fork is a small one, and I wrote it up separately as an Ideas post: https://github.com/orgs/RocketChat/discussions/42188. This post is the "why" and the "does it actually work."
The problem
A chat server's most sensitive data is the message text itself. Today Rocket.Chat gives you two options and each leaves a gap:
message.msgsits in Mongo as readable text. TLS and disk encryption help in transit and at the volume level, but a stolen database, a leaked backup, or anyone with a Mongo connection reads every message.I wanted the middle ground most self-hosters actually ask for: make the database and backups useless to a thief, gate reads by a role someone can grant and revoke, and keep the product working.
What it does
The key is held by nobody. It lives as threshold shares across the Tide network and is never reassembled, not even to decrypt. The server asks the network to open a field on behalf of a named user, and the network only agrees if a quorum-granted role says that user may read.
rocketchat_messagecollection or a leaked backup reveals nothing.How it works (three small seams)
BaseRaw(insertOne/updateOne), somsgis sealed before it hits Mongo. Routing keys (rid,u._id,ts) stay in the clear so the app keeps working.normalizeMessagesForUser, which nearly every REST and method read of messages already passes through, so history, search, threads and pins are all covered in one place. It opens as the requesting user, gated by their role.TransformMessageseam in the streamer (the one__my_messages__already uses). Live delivery is a shared fan-out, so a message is opened once per connected recipient: granted members see plaintext arrive live, an ungranted viewer in the same room sees ciphertext.Does it actually work
Yes, end to end on Rocket.Chat 7.x:
rocketchat_message):ms1:AQAAAAEA...ciphertext.ms1:...ciphertext.The honest tradeoff
E2EE gives a stronger "even a malicious server operator cannot read" guarantee, because it trusts only the device. This mode, in its lightweight form, trusts the app server to say who is calling (there is a stronger per-request enclave path if you need it). So E2EE is the right choice when you do not trust the server at all and only the two participants should ever read. This is the right choice when you want a stolen database to be worthless, reads to be governed and revocable, and search, bots, notifications and compliance to keep working.
One line: E2EE hides messages from everyone including your own org, the default hides them from no one, this hides them from a database thief while a quorum decides who may read.
Disclaimer: this is the lightweight form
What I have shown here is the lightweight, self-hostable form of the approach: minidauth, running "tideless", where the app vouches for who is calling and a quorum-granted role gates the read. It is a proof of concept, not production hardened, and per-field round trips are the demo shape rather than the envelope-encryption shape a real deployment would use.
The enterprise-grade version of this is TideCloak, Tide's identity and access product. There, who is calling is proven per request with threshold cryptography inside the network rather than asserted by the app server, which closes the "trust the app server" gap called out above. minidauth is the small on-ramp to that model, meant to show the shape and let you try it without adopting the full stack. Treat this fork as a demonstration, and TideCloak as the path if you want to run field-level, quorum-governed sealing for real.
Try it / the code
MINIDAUTH_SEAL_URLis set, and it is a proof of concept (per-field round trips today; envelope encryption is the production shape).Feedback welcome, especially on the seam proposal in the Ideas post. If that lands, this stops being a fork and becomes a plugin.
All reactions