@nhtio/rocketchat-server-side-e2ee
v0.1.0-master-3eb6a4c9
Published
This library provides a drop-in API for allowing RocketChat Apps-Engine Apps to participate in conversations using end-to-end encryption (E2EE).
Downloads
133
Readme
RocketChat Server Side End to End Encryption
This library provides a drop-in API for allowing RocketChat Apps-Engine Apps to participate in conversations using end-to-end encryption (E2EE).
Security Warning
This implementation allows a server-side application to decrypt encrypted messages in an encrypted conversation. Applications implementing this library should be explicit about their usage with clear reasoning for its usage and purpose. App consumers should be explicitly warned about the security implications of allowing programmatic access to encrypted conversations so they can evaluate the risks before installing and using the application.
Why Apps-Engine Apps Cannot Participate Today
The server cannot decrypt (by design)
- The room key is never available to the server in plaintext.
- Server stores per-user encrypted room keys; only the client with the RSA private key can decrypt.
Apps-Engine surface does not expose needed fields
Even if an app wanted to decrypt:
- Apps-Engine message hooks receive an
IMessageabstraction. - The E2EE ciphertext payload is stored in message
content(server-side document), but Apps-Engine does not expose that field via its interfaces. - Apps-Engine also does not expose subscription key fields (
E2EKey,E2ESuggestedKey, key history).
Crypto runtime mismatch
- Rocket.Chat's E2EE implementation uses browser WebCrypto.
- Apps-Engine apps run server-side and would need a Node crypto implementation.
Key distribution requires identity
- Room keys are encrypted to recipients’ RSA public keys.
- An app would need a distinct keypair and be treated as a room participant/device.
E2EE architecture in Rocket.Chat
This section is a technical reference for how E2EE is implemented in Rocket.Chat core. Understanding this architecture is necessary to build a compatible server-side participant.
Where E2EE lives
The actual cryptographic operations and key handling are entirely client-side, implemented in Meteor client code under apps/meteor/client/lib/e2ee/:
- Orchestration:
rocketchat.e2e.ts— top-level E2EE lifecycle - Per-room encrypt/decrypt:
rocketchat.e2e.room.ts - Keychain / private-key wrapping:
keychain.ts - Encrypted content parsing (v1/v2):
content.ts - WebCrypto wrappers:
crypto/aes.ts,crypto/rsa.ts,crypto/pbkdf2.ts,crypto/shared.ts
The server never sees plaintext. Server-side code under apps/meteor/app/e2e/server/ only stores public keys, encrypted private keys, and relays per-user encrypted room keys:
- Key upload/download:
methods/fetchMyKeys.ts,methods/setUserPublicAndPrivateKeys.ts - Room key distribution / suggested keys:
methods/updateGroupKey.ts,methods/requestSubscriptionKeys.ts,functions/provideUsersSuggestedGroupKeys.ts - Room key ID and rotation:
methods/setRoomKeyID.ts,functions/resetRoomKey.ts
The REST API surface is defined in apps/meteor/app/api/server/v1/e2e.ts.
Crypto primitives
Rocket.Chat uses the browser's Web Crypto API (window.crypto.subtle) with the following algorithms:
- User keypair: RSA-OAEP 2048-bit, SHA-256
- Private key protection:
- Current: AES-GCM (256-bit) + PBKDF2 SHA-256 (100,000 iterations)
- Legacy: AES-CBC + PBKDF2 (lower iteration count)
- Room session/group key: AES-GCM (256-bit)
- Message encryption: AES-GCM with random 12-byte IV
- Files: AES-CTR with per-file key + SHA-256 integrity metadata
Key management and distribution
User keys — The server stores the user's plaintext public key (JWK) and an encrypted private key blob (AES-GCM wrapped with the user's passphrase). The client decrypts the private key using the passphrase and holds the plaintext key only in memory.
Room (group/session) keys — Each room has a key ID (rooms.e2eKeyId). Every member receives a copy of the room's AES session key encrypted to their RSA public key, stored in per-user subscription fields (subscriptions.E2EKey, subscriptions.E2ESuggestedKey).
When a new member joins:
- The server marks them as needing a key.
- Any existing member who already has the room key encrypts it to the new member's public key and posts it as a suggested key.
- The new member accepts after verifying they can decrypt.
On rotation, the server generates a new key ID and invalidates the current key. Old keys are preserved client-side via per-user history so older messages can still be decrypted.
Message format (wire envelope)
The server stores only the ciphertext envelope; plaintext is never persisted. The current (v2) encrypted message shape is:
t: 'e2e'— message type markere2e: 'pending' | 'done'— encryption statuscontent— the encrypted envelope:algorithm: e.g.rc.v2.aes-sha2kid: room key UUIDiv: base64-encoded initialization vectorciphertext: base64-encoded ciphertext
The msg field is absent or empty for encrypted messages.
What this library provides
The goal is to treat an Apps-Engine app as a cryptographic participant — giving it a managed identity, access to room keys, and the crypto primitives needed to read and write encrypted messages.
App identity and key management
An app is introduced as a first-class E2EE participant with its own RSA-OAEP keypair managed server-side. The private key should be protected via a KMS or secret store; encrypted-at-rest is the minimum acceptable baseline. Each app identity is scoped to a specific set of rooms it is authorized to participate in.
- [ ] Generate and store an RSA-OAEP keypair per app
- [ ] Protect the private key (KMS / secret store / encrypted-at-rest)
- [ ] Associate app identity with an app id and an authorized room list
- [ ] Implement key rotation flow
- [ ] Implement key revocation flow
Expose encrypted payloads to Apps-Engine (read path)
Apps-Engine message hooks currently receive an IMessage abstraction that does not expose the E2EE ciphertext stored in the server-side content field. To allow an app to decrypt, the encrypted content envelope must be surfaced.
- [ ] Extend Apps-Engine
IMessageto includeencryptedContent?: { algorithm: string; kid: string; iv: string; ciphertext: string }fort:'e2e'messages - [ ] Add runtime changes to populate the field when delivering messages to apps
- [ ] Version the capability so older apps are not broken
Expose key distribution and key store APIs to Apps-Engine
An app needs access to the AES session key for each room it participates in. The recommended model is direct key provisioning: when a room is encrypted and the app is authorized, the server delivers the current room key encrypted to the app’s public key and notifies the app on rotation. An alternative is to let the app appear in the E2ESuggestedKey waiting queue and receive keys through the standard client flow.
- [ ] Implement
getEncryptedRoomKey(roomId, kid)API for apps - [ ] Emit key rotation events to authorized apps
- [ ] Expose key history API for decrypting older messages
Node-side crypto compatible with the RC client format
The RC client uses browser WebCrypto. Server-side crypto must produce and consume the same wire format so that client-encrypted messages can be decrypted by the app and vice versa.
- [ ] Implement RSA-OAEP encrypt/decrypt with SHA-256
- [ ] Implement AES-GCM encrypt/decrypt
- [ ] Implement wire envelope parsing and encoding matching
apps/meteor/client/lib/e2ee/content.ts - [ ] Handle EJSON serialization of plaintext payloads
- [ ] Write interop tests: client-encrypt ↔ server-decrypt and server-encrypt ↔ client-decrypt
Apps-Engine E2EE send support (write path)
An authorized app should be able to send an encrypted message to an encrypted room. This can be exposed as a high-level sendEncryptedMessage(roomId, plaintext) method that the server encrypts and stores as a t:'e2e' message, or as a lower-level API that lets the app build the envelope itself.
- [ ] Implement send API (high-level or envelope-based)
- [ ] Enforce that only authorized apps with a valid room key can send
- [ ] Ensure plaintext is never passed to message indexing or search
