FEATURE: Encrypted drafts for new private messages.

FEATURE: Encrypted drafts for new private messages.

From bbf2b2f92937e52cdbb5b4e655e43603d0438b14 Mon Sep 17 00:00:00 2001
From: Dan Ungureanu <dan@ungureanu.me>
Date: Fri, 30 Nov 2018 18:00:04 +0200
Subject: [PATCH] FEATURE: Encrypted drafts for new private messages.


diff --git a/assets/javascripts/discourse/initializers/hook-composer.js.es6 b/assets/javascripts/discourse/initializers/hook-composer.js.es6
index 4268da9..94c084d 100644
--- a/assets/javascripts/discourse/initializers/hook-composer.js.es6
+++ b/assets/javascripts/discourse/initializers/hook-composer.js.es6
@@ -8,6 +8,7 @@ import {
 import {
   encrypt,
   decrypt,
+  rsaDecrypt,
   exportKey,
   importKey,
   generateKey,
@@ -28,33 +29,54 @@ export default {
     // edited or a draft is loaded.
     const appEvents = container.lookup("app-events:main");
     appEvents.on("composer:reply-reloaded", model => {
-      let topicId;
+      let decTitle, decReply;
+
+      if (model.get("draftKey") === Composer.NEW_PRIVATE_MESSAGE_KEY) {
+        /*
+         * Decrypt private message drafts.
+         */
+        const p = getPrivateKey();
+        decTitle = p.then(key => rsaDecrypt(key, model.get("title")));
+        decReply = p.then(key => rsaDecrypt(key, model.get("reply")));
+      } else {
+        /*
+         * Decrypt replies.
+         */
+        let topicId;
+
+        // Try get topic ID from topic model.
+        const topic = model.get("topic");
+        if (topic) {
+          topicId = topic.get("id");
+        }
 
-      // Try get topic ID from topic model.
-      const topic = model.get("topic");
-      if (topic) {
-        topicId = topic.get("id");
-      }
+        // Try get topic ID from draft key.
+        if (!topicId) {
+          const draftKey = model.get("draftKey");
+          if (draftKey && draftKey.indexOf("topic_") === 0) {
+            topicId = draftKey.substring("topic_".length);
+          }
+        }
 
-      // Try get topic ID from draft key.
-      if (!topicId) {
-        const draftKey = model.get("draftKey");
-        if (draftKey && draftKey.indexOf("topic_") === 0) {
-          topicId = draftKey.substring("topic_".length);
+        if (hasTopicKey(topicId)) {
+          decTitle = getTopicTitle(topicId);
+          const reply = model.get("reply");
+          if (reply) {
+            decReply = getTopicKey(topicId).then(key => decrypt(key, reply));
+          }
         }
       }
 
-      if (hasTopicKey(topicId)) {
-        getTopicTitle(topicId)
-          .then(t => model.set("title", t))
+      if (decTitle) {
+        decTitle
+          .then(title => model.setProperties({ title, isEncrypted: true }))
           .catch(() => {});
+      }
 
-        getTopicKey(topicId).then(key => {
-          const reply = model.get("reply");
-          if (reply) {
-            decrypt(key, reply).then(r => model.set("reply", r));
-          }
-        });
+      if (decReply) {
+        decReply
+          .then(reply => model.setProperties({ reply, isEncrypted: true }))
+          .catch(() => {});
       }
     });
 
diff --git a/assets/javascripts/discourse/initializers/hook-draft.js.es6 b/assets/javascripts/discourse/initializers/hook-draft.js.es6
index 233e106..73c08c9 100644
--- a/assets/javascripts/discourse/initializers/hook-draft.js.es6
+++ b/assets/javascripts/discourse/initializers/hook-draft.js.es6
@@ -1,8 +1,13 @@
 import {
   hasTopicKey,
-  getTopicKey
+  getTopicKey,
+  getPublicKey
 } from "discourse/plugins/discourse-encrypt/lib/discourse";
-import { encrypt } from "discourse/plugins/discourse-encrypt/lib/keys";
+import {
+  encrypt,
+  rsaEncrypt
+} from "discourse/plugins/discourse-encrypt/lib/keys";
+import Composer from "discourse/models/composer";
 import Draft from "discourse/models/draft";
 
 export default {
@@ -13,22 +18,40 @@ export default {
       save(key, sequence, data) {
         // TODO: https://github.com/emberjs/ember.js/issues/15291
         let { _super } = this;
+        let encTitle, encReply;
 
-        if (key.indexOf("topic_") === 0) {
+        if (key === Composer.NEW_PRIVATE_MESSAGE_KEY) {
+          /*
+           * Encrypt private message drafts.
+           */
+          // TODO: Avoid using the container.
+          const container = Discourse.__container__;
+          const controller = container.lookup("controller:composer");
+          if (controller.get("model.isEncrypted")) {
+            const p = getPublicKey();
+            encTitle = p.then(publicKey => rsaEncrypt(publicKey, data.title));
+            encReply = p.then(publicKey => rsaEncrypt(publicKey, data.reply));
+          }
+        } else if (key.indexOf("topic_") === 0) {
+          /*
+           * Encrypt replies.
+           */
           const topicId = key.substr("topic_".length);
-
           if (hasTopicKey(topicId)) {
-            const p0 = getTopicKey(topicId);
-            const p1 = p0.then(topicKey => encrypt(topicKey, data.title));
-            const p2 = p0.then(topicKey => encrypt(topicKey, data.reply));
-            Promise.all([p1, p2]).then(([title, reply]) => {
-              data.title = title;
-              data.reply = reply;
-              return _super.call(this, ...arguments);
-            });
+            const p = getTopicKey(topicId);
+            encTitle = p.then(topicKey => encrypt(topicKey, data.title));
+            encReply = p.then(topicKey => encrypt(topicKey, data.reply));
           }
         }
 
+        if (encTitle && encReply) {
+          return Promise.all([encTitle, encReply]).then(([title, reply]) => {
+            data.title = title;
+            data.reply = reply;
+            return _super.call(this, ...arguments);
+          });
+        }
+
         return _super.call(this, ...arguments);
       }
     });
diff --git a/assets/javascripts/lib/discourse.js.es6 b/assets/javascripts/lib/discourse.js.es6
index 691d92e..28196ab 100644
--- a/assets/javascripts/lib/discourse.js.es6
+++ b/assets/javascripts/lib/discourse.js.es6
@@ -17,6 +17,11 @@ export const ENCRYPT_ENABLED = 1;
 export const ENCRYPT_ACTIVE = 2;
 
 /**
+ * @var User's public key used to encrypt topic keys and drafts for private message.
+ */
+let publicKey;
+
+/**
  * @var User's private key used to decrypt topic keys.
  */
 let privateKey;
@@ -32,18 +37,41 @@ const topicKeys = {};
 const topicTitles = {};
 
 /**
+ * Gets a user's key pair from the database and caches it for future usage.
+ *
+ * @return Tuple of two public and private CryptoKey.
+ */
+export function getKeyPair() {
+  return loadKeyPairFromIndexedDb().then(keyPair => {
+    if (!keyPair || !keyPair[0] || !keyPair[1]) {
+      return Promise.reject();
+    }
+
+    [publicKey, privateKey] = keyPair;
+    return keyPair;
+  });
+}
+
+/**
+ * Gets user's public key.
+ *
+ * @return CryptoKey
+ */
+export function getPublicKey() {
+  return publicKey
+    ? Promise.resolve(publicKey)
+    : getKeyPair().then(keyPair => keyPair[0]);
+}
+
+/**
  * Gets user's private key.
  *
  * @return CryptoKey
  */
 export function getPrivateKey() {
-  if (privateKey) {
-    return Promise.resolve(privateKey);
-  }
-
-  return loadKeyPairFromIndexedDb().then(keyPair =>
-    (privateKey = keyPair[1]) ? privateKey : Promise.reject()
-  );
+  return privateKey
+    ? Promise.resolve(privateKey)
+    : getKeyPair().then(keyPair => keyPair[1]);
 }
 
 /**
diff --git a/assets/javascripts/lib/keys.js.es6 b/assets/javascripts/lib/keys.js.es6
index 6d88038..178ff6b 100644
--- a/assets/javascripts/lib/keys.js.es6
+++ b/assets/javascripts/lib/keys.js.es6
@@ -296,3 +296,35 @@ export function decrypt(key, ciphertext) {
     .decrypt({ name: "AES-CBC", iv: iv }, key, encrypted)
     .then(buffer => bufferToString(buffer));
 }
+
+/**
+ * Encrypts a message with a RSA public key.
+ *
+ * @param key
+ * @param plaintext
+ *
+ * @return String
+ */
+export function rsaEncrypt(key, plaintext) {
+  const buffer = stringToBuffer(plaintext);
+
+  return window.crypto.subtle
+    .encrypt({ name: "RSA-OAEP", hash: { name: "SHA-256" } }, 

GitHub

2 Likes