Discourse Encrypt (for Private Messages)

Discourse Encrypt :key:

Discourse Encrypt is a plugin that enables private, encrypted messaging between end-users. All sensitive information is stored securely on the server and is encrypted and decrypted only on the client-side.

Three easy steps to use this plugin:

  1. enable encryption (generate new private key) and activate it (on current device)

  1. send an encrypted message (to a user who also enabled encryption)

  1. read decrypted messages

Note: user was prompted for password again because encryption was deactivated (by logging out or explicitly deactivating it from preferences screen).

How do I install it?

Follow Install a Plugin, using git clone https://github.com/discourse/discourse-encrypt.git as the plugin command.

Basically, edit your app.yml file to include the command specified before.

Technical information

This plugin gives users the possibility to communicate securely through Discourse, by using an end-to-end encryption scheme. Most of plugin’s logic is implemented on the client-side and the server-side handles only public or encrypted information. It does not encrypt any post metadata, such as names of participants in the conversation, posted time, likes, small actions, etc; uploads are encrypted, but their presence is not because the system must associate uploads with posts to prevent deleting them.

The whole code is open-source and security enthusiasts are welcome to review it. For any further information, do not hesitate to contact me or the team. :slight_smile:


The goal of this plugin is to offer integrity and confidentiality of the encrypted contents, and to protect it against information leaks and unauthorized users. The following sections describe the usual operation mode, used algorithms and threat models.

To use this system, users enroll once by generating a “user identity” consisting of two 4096-bits RSA keys, one for encryption and another for signing. Users can export their “identity” for safe keeping or store it on the server, encrypted after generating a paper key. These two methods serve as backups or are used to enroll new devices.

Paper keys (inspired by RFC 1751 and BIP-39) are human-readable keys that are used to securely store the “user identity” on the server. A paper key consists of 12 random words, picked from a list of 2048 words, offering 121-bits of entropy (the first word is used as a label). To encrypt the “user identity” with a paper key, the system will first derive the encryption key using PBKDF2 to stretch the paper key into a 256-bit AES-GCM key.

Creating an encrypted post

To create a new post, the user (their browser) will:

  1. sign the current post content using their private signing key;

  2. generate a new “topic key” (a AES-256-GCM key) - which is going to be used to encrypt the post, some post metadata and the title of the new topic (if available);

  3. fetch the public keys of all participants and encrypt the “topic key” for each of them;

  4. send to the server the encrypted post (Base64 encoded) and the encrypted topic keys (also Base64 encoded) of each participant.

The pseudocode for the encryption operation would be something like:

signature = rsa_pss_sign(current_user.identity.sign_key.private, post.raw) # 1
topic_key = topic.key || generate_aes_256_gcm_key() # 2
encrypted_title = aes_256_gcm_encrypt(topic.title, topic_key) if topic.blank?
encrypted_post = aes_256_gcm_encrypt(signature + post.raw, topic_key)
encrypted_topic_keys = recipients.map { |r| rsa_oaep_encrypt(topic_key, r.identity.encryption_key.public) } # 3
$.put("/posts/create", { title: encrypted_title, raw: encrypted_post, keys: encrypted_topic_keys }) # 4

Reading an encrypted post

To read a post, the user (their browser) will:

  1. fetch the encrypted post payload (post plaintext and signature) and encrypted topic key;

  2. use their private encryption key to decrypt the encrypted topic key;

  3. use the decrypted topic key to decrypt the encrypted post payload;

  4. fetch the public signing key of the poster and verify the post signature.

Algorithm Suite

This plugin makes extensive use of the cryptographic primitives implemented in Web Crypto API, which are available in any of the modern browsers Discourse supports (except Internet Explorer).

  • getRandomValues PRNG: It generates paper keys and 96-bits random IVs.

  • PBKDF2: It stretches the 132-bit paper keys to 256-bit keys, used for encrypting “user identities”.

  • AES-256-GCM: Used to encrypt each post’s content. It also offers authentication by producing an authentication tag of 128-bits, but this is a less important aspect because posts are verified using a signature generated by the poster (see step 1 above).

  • RSA-OAEP: Used to encrypt “topic keys” and “user identites” for safe-keeping on the server. All RSA-OAEP keys are 4096-bits long.

  • RSA-PSS: Used to sign each post’s content for verifying authenticity. All RSA-PSS keys are 4096-bits long.


The system uses a set of primitives built on top of those provided by the browser via Web Cryptography API.

  • encrypt and decrypt: Used to encrypt and decrypt post contents. encrypt takes a JSON, an AES-256-GCM key and a RSA-PSS public key and outputs a single Base64 encoded string; decrypt takes a Base64 encoded string and an AES-256-GCM key and outputs the initial JSON object;

  • verify: Used to verify post contents after decryption;

  • exportKey and importKey: Used to export and import “topic keys”;

  • exportIdentity and importIdentity: Used to export and import “user identities”.

Types of keys:

  • topic keys (AES-256-GCM)

  • RSA key-pair (public and private keys) (RSA-OAEP and RSA-PSS, 4096-bits)

    • used to encrypt all topic keys a user has access to
    • are generated per user on the client-side by the original poster using WebCrypto’s API generateKey primitive and shared amongst all of user’s devices
    • server-side: public identity is stored as exported by the client, but the private identity will always be encrypted with the passphrase key
    • client-side: public and private keys are stored as CryptoKey in IndexedDb; if not possible, it will use window.localStorage (in Safari)
  • passphrase keys (derived using PKBDF2 with 128,000 iterations)

    • used to encrypt “user identities” for safe storage on the server
    • derived from a paper key (or user’s passphrase for legacy purposes)

Threat models

Compromised Discourse Instance

An attacker which can inject code could in theory access encrypted information by serving malicious code, which decrypts the encrypted content and sends the plaintext posts to another server. To make this possible, it is enough to have access to an administrator account and create a theme component with the malicious code.

Default protection mechanisms such as CSP can detect and mitigate Cross Site Scripting (XSS) attacks that could also represent a way of injecting malicious code.

Man-in-the-Middle Attack

In Man-in-the-Middle attacks, the attacker intercepts the communication between the user and server, giving them the ability to read or alter it. Because the plugin encrypts everything before sending, an attacker cannot decrypt anything by simply eavesdropping. Similarly, because information is authenticated the attacker cannot alter it.

However, the attacker could serve malicious code back to the user and follow a similar attack to the one presented in the previous section. This is partially mitigated by HTTPS, which is considerably reducing the attack probability.


The plugin already has a little history and that can be seen while browsing the source code and noticing the two implementations of the protocol v0 (initial, alpha-beta release) and v1. Protocol v0 is no longer used to encrypt new posts, but kept to continue decrypting old ones. New protocol includes authenticity of the ciphertexts and all posts are signed with the private key of the poster.

Other Resources


You know how admins can read users’ messages? Admins can’t check and read users’ encrypted messages right?


Absolutely amazing! :clap: Definitely something I‘ll try out …

Not unless they have the passphrase to access your keys, no.

A malicious admin could compromise everything from the server up though. You can’t trust a system any more than you can the people running it.


This makes it much harder for admins to read user messages. The messages themselves are encrypted in the DB with this plugin. So yes, the threat of “random admin decides to snoop on PMs” is vastly mitigated here.*

You still have to somewhat trust admins not to mount an attack, as they still have the ability to inject arbitrary scripts via themes. But it is much less trivial to do.


Although arguably if they can inject code to intercept the passphrase they can already also read keypresses in the PM.

A system is only as strong as the weakest component.


Indeed. Maybe it‘s necessary to implement a code validation mechanism for this plugin and important messaging components to ensure, admins and users have independent 3rd-parties that validates the source code.

I‘m not a big fan of buzzwords but this is a good entry point for decentralized finger-printing with a blockchain technology :wink:

Very true. The short term plan here is to try and get feedback from security professionals about the underlying protocols and patterns used.

We want to ensure that as long as admins are not fussing with JS payloads stuff will remain safe.

Long term if this gets popular someone can ship an electron app which ensures only validated JS runs or a browser plugin that takes care of that.


Anything which keeps it mobile friendly gets my vote.

I have the PHI/HIPAA conversation very regularly where Discourse is concerned. Looking forward to having more of the boxes ticked.


Maybe some day, the official Discourse app will do the same. :wink: Especially due to the iOS limitation of sending/receiving push notifications, it would be interesting to integrate a messanger inside that app. Something that feels much more native.

1 Like

Notifications are available to hosted customers. You can also deliver notifications to iOS using the single-site app and OneSignal.

1 Like

If anyone wants to have a play with this plugin you can try it out at: https://try-encrypt.discourse.org/t/how-to-try-out-discourse-encrypt/11


@sam, there’s weird grammar here in the intro topic:

If you want to also add me to the test a message feel free to add @sam, but fair warning I am unlikely to bother checking it and emails are disabled here anyway.

Instead it should be

If you want to also add me to the test a message feel free to add @sam, but fair warning I am unlikely to bother checking it and emails are disabled here anyway.


I’ve been using this a bit before it was talked about here. Found it in the repo and been playing with it.
I’m not an expert but I think so far it’s great. This helps security and privacy conscious sites a lot!


Is there a way to “reset” a user so they can reconfigure their password if they forgot it?

I’ve looked a bit and am not finding it. It’s pretty funny cause it’s a staff on this test site and I’m like … good job making a dumb password you forgot. lol

AFAIK no, if they lose the passphrase to decrypt their key the key itself is lost.

If it wasn’t then a compromised account could reset a passphrase and access encrypted data.

What about reset as in burning the key and making them generate a new one knowing they won’t ever be able to access their old messages?
That should be safe as far as “resetting” goes at least it is when it comes to keysystems in general?

Like allowing the user to disable and reinstalize encryption and keygen if they lose that password. It would always have to come at the cost of lost data but it wouldn’t lock a user account out of ever using e2e. Probably on list but not currently implemented.

The users and staff on my test server running this however love the simplicity of how it currently is. Thanks to the dev and team for this work!


I have mixed feelings about this one. On one hand, it would be great if users could actually reset their key in case they forget it. On the other hand, reseting the key will lead to losing access to previous encrypted messages. This is definitely a dangerous operation and there would be no way of going back.


Wouldn’t having that feature also introduce a lot of complexity to private messages? (i.e. You see all messages in the private messages list but you can only access these X messages since you changed your key). Could create some solid usability issues long-term if people lose their keys or need to change them regularly in that it won’t be clear what messages you have access to or don’t.

1 Like

Would it help to follow a pattern of an industry where encrpyted private messaging is already a thing?

In healthcare it’s quite common for doctors of various disciplines to use an encryption product on top of their email service. They aren’t actually sending encrpyted email though, it all lives within a web app that integrates with their webmail.

So, much like a discourse PM, when an encrpyted message is sent, the recipient only receives a notification stating that there’s a message awaiting collection. Providing they remember their password, they can visit and read any messages with one additional credential challenge.

In the event the password is forgotten they can request a secure message password reset, which requires approval from someone else at the practice. Nobody at the practice can reissue access unless the client has first requested a reset and confirmed said request by clicking a link in a message sent to their account email.

We’ve already acknowledged that if the site has been compromised by a malicious admin that there’s very little that can be done to mitigate from such attacks and this is no different, the process could be keyed against the email address to prevent someone reaching into the database and changing the associated email address. Unfortunately that wouldn’t do anything to prevent a determined bad actor from intercepting the message to get at the link and they could even go so far as to block the message in-transit.

Because we’re using per-topic keys though, that would mean needing approval from a participant of each encrypted topic, wouldn’t it?