npm package: @nhtio/rocketchat-server-side-e2ee
Description: This library provides a drop-in API for allowing RocketChat Apps-Engine Apps to participate in conversations using end-to-end encryption (E2EE).
Author: Jak Giveon
Latest version: 0.1.0-master-3eb6a4c9

# 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 `IMessage` abstraction.
- 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:

1. The server marks them as needing a key.
2. 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.
3. 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 marker
- `e2e: 'pending' | 'done'` — encryption status
- `content` — the encrypted envelope:
  - `algorithm`: e.g. `rc.v2.aes-sha2`
  - `kid`: room key UUID
  - `iv`: base64-encoded initialization vector
  - `ciphertext`: 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 `IMessage` to include `encryptedContent?: { algorithm: string; kid: string; iv: string; ciphertext: string }` for `t:'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

## References

- [Rocket.Chat E2EE client directory](https://github.com/RocketChat/Rocket.Chat/tree/develop/apps/meteor/client/lib/e2ee)
- [Rocket.Chat E2EE server directory](https://github.com/RocketChat/Rocket.Chat/tree/develop/apps/meteor/app/e2e/server)
- [Rocket.Chat E2EE REST API](https://github.com/RocketChat/Rocket.Chat/blob/develop/apps/meteor/app/api/server/v1/e2e.ts)