FIX: Encrypt each upload with its own key

FIX: Encrypt each upload with its own key

diff --git a/assets/javascripts/discourse/initializers/hook-decrypt-post.js.es6 b/assets/javascripts/discourse/initializers/hook-decrypt-post.js.es6
index 7cc21f6..de3bf83 100644
--- a/assets/javascripts/discourse/initializers/hook-decrypt-post.js.es6
+++ b/assets/javascripts/discourse/initializers/hook-decrypt-post.js.es6
@@ -17,6 +17,8 @@ import {
 import { withPluginApi } from "discourse/lib/plugin-api";
 import showModal from "discourse/lib/show-modal";
 import { cookAsync } from "discourse/lib/text";
+import { imageNameFromFileName } from "discourse/lib/uploads";
+import { base64ToBuffer } from "discourse/plugins/discourse-encrypt/lib/base64";
 import {
   ENCRYPT_DISABLED,
   getDebouncedUserIdentities,
@@ -134,6 +136,21 @@ function downloadEncryptedFile(url, keyPromise) {
 }
 
 function resolveShortUrlElement($el) {
+  const topicId = $el.closest("[data-topic-id]").data("topic-id");
+  const keyPromise = $el.data("key")
+    ? new Promise((resolve, reject) => {
+        window.crypto.subtle
+          .importKey(
+            "raw",
+            base64ToBuffer($el.data("key")),
+            { name: "AES-GCM", length: 256 },
+            true,
+            ["encrypt", "decrypt"]
+          )
+          .then(resolve, reject);
+      })
+    : getTopicKey(topicId);
+
   if ($el.prop("tagName") === "A") {
     const data = lookupCachedUploadUrl($el.data("orig-href"));
     const url = data.short_path;
@@ -145,31 +162,28 @@ function resolveShortUrlElement($el) {
 
     if (url !== MISSING) {
       $el.attr("href", url);
-      const content = $el.text().split("|");
-
-      if (content[1] === ATTACHMENT_CSS_CLASS) {
-        $el.addClass(ATTACHMENT_CSS_CLASS);
-        $el.text(content[0].replace(/\.encrypted$/, ""));
-        if (content[0].match(/\.encrypted$/)) {
-          $el.on("click", () => {
-            const topicId = $el.closest("[data-topic-id]").data("topic-id");
-            const keyPromise = getTopicKey(topicId);
-            downloadEncryptedFile(url, keyPromise).then(file => {
-              const a = document.createElement("a");
-              a.href = window.URL.createObjectURL(file.blob);
-              a.download = file.name || content[0];
-              a.download = a.download.replace(/\.encrypted$/, "");
-              a.style.display = "none";
-
-              document.body.appendChild(a);
-              a.click();
-              document.body.removeChild(a);
-
-              window.URL.revokeObjectURL(a.href);
-            });
-            return false;
+
+      const isEncrypted = $el.text().match(/\.encrypted$/) || $el.data("key");
+
+      if (isEncrypted && $el.hasClass(ATTACHMENT_CSS_CLASS)) {
+        $el.text($el.text().replace(/\.encrypted$/, ""));
+
+        $el.on("click", () => {
+          downloadEncryptedFile(url, keyPromise).then(file => {
+            const a = document.createElement("a");
+            a.href = window.URL.createObjectURL(file.blob);
+            a.download = file.name || $el.text();
+            a.download = a.download.replace(/\.encrypted$/, "");
+            a.style.display = "none";
+
+            document.body.appendChild(a);
+            a.click();
+            document.body.removeChild(a);
+
+            window.URL.revokeObjectURL(a.href);
           });
-        }
+          return false;
+        });
       }
     }
   } else if ($el.prop("tagName") === "IMG") {
@@ -182,11 +196,15 @@ function resolveShortUrlElement($el) {
     $el.removeAttr("data-orig-src");
 
     if (url !== MISSING) {
-      if ($el.attr("alt").match(/\.encrypted$/)) {
-        const topicId = $el.closest("[data-topic-id]").data("topic-id");
-        const keyPromise = getTopicKey(topicId);
+      const isEncrypted =
+        $el.attr("alt").match(/\.encrypted$/) || $el.data("key");
+
+      if (isEncrypted) {
         return downloadEncryptedFile(url, keyPromise).then(file => {
-          $el.attr("alt", $el.attr("alt").replace(/\.encrypted$/, ""));
+          const imageName = file.name
+            ? imageNameFromFileName(file.name)
+            : $el.attr("alt").replace(/\.encrypted$/, "");
+          $el.attr("alt", imageName);
           $el.attr("src", window.URL.createObjectURL(file.blob));
         });
       } else {
diff --git a/assets/javascripts/discourse/initializers/hook-encrypt-upload.js.es6 b/assets/javascripts/discourse/initializers/hook-encrypt-upload.js.es6
index 549081a..897b870 100644
--- a/assets/javascripts/discourse/initializers/hook-encrypt-upload.js.es6
+++ b/assets/javascripts/discourse/initializers/hook-encrypt-upload.js.es6
@@ -1,11 +1,13 @@
 import { withPluginApi } from "discourse/lib/plugin-api";
+import { getUploadMarkdown, isAnImage } from "discourse/lib/uploads";
+import { bufferToBase64 } from "discourse/plugins/discourse-encrypt/lib/base64";
 import {
   ENCRYPT_ACTIVE,
   getEncryptionStatus,
-  getTopicKey,
   hasTopicKey
 } from "discourse/plugins/discourse-encrypt/lib/discourse";
-import { getUploadMarkdown, isAnImage } from "discourse/lib/uploads";
+import { DEFAULT_LIST } from "pretty-text/white-lister";
+import { Promise } from "rsvp";
 
 export default {
   name: "hook-encrypt-upload",
@@ -17,84 +19,110 @@ export default {
     }
 
     withPluginApi("0.8.31", api => {
-      const localData = {};
+      DEFAULT_LIST.push("a[data-key]");
+      DEFAULT_LIST.push("img[data-key]");
+
+      const uploadsKeys = {};
+      const uploadsData = {};
 
       api.addComposerUploadHandler([".*"], (file, editor) => {
         const controller = container.lookup("controller:composer");
         const topicId = controller.get("model.topic.id");
-
-        if (!hasTopicKey(topicId)) {
-          if (controller.get("model.isEncrypted")) {
-            // Cannot encrypt uploads for new topics.
-            bootbox.alert(I18n.t("encrypt.encrypted_uploads"));
-            return false;
-          }
+        if (!controller.get("model.isEncrypted") && !hasTopicKey(topicId)) {
           return true;
         }
 
-        if (isAnImage(file.name)) {
-          const img = new Image();
-          img.onload = function() {
-            const ratio = Math.min(
-              Discourse.SiteSettings.max_image_width / img.width,
-              Discourse.SiteSettings.max_image_height / img.height
-            );
-
-            localData[file.name] = {
-              original_filename: file.name,
-              width: img.width,
-              height: img.height,
-              thumbnail_width: Math.floor(img.width * ratio),
-              thumbnail_height: Math.floor(img.height * ratio)
-            };
-
-            // TODO: Save object URL to be used in composer
-          };
-          img.src = window.URL.createObjectURL(file);
-        } else {
-          localData[file.name] = { original_filename: file.name };
-        }
+        const dataPromise = isAnImage(file.name)
+          ? new Promise((resolve, reject) => {
+              const img = new Image();
+              img.onload = () => resolve(img);
+              img.onerror = err => reject(err);
+              img.src = window.URL.createObjectURL(file);
+            }).then(img => {
+              const ratio = Math.min(
+                Discourse.SiteSettings.max_image_width / img.width,
+                Discourse.SiteSettings.max_image_height / img.height
+              );
+
+              return {
+                original_filename: file.name,
+                width: img.width,
+                height: img.height,
+                thumbnail_width: Math.floor(img.width * ratio),
+                thumbnail_height: Math.floor(img.height * ratio)
+              };
+            })
+          : Promise.resolve({ original_filename: file.name });
+
+        const decryptedPromise = new Promise((resolve, reject) => {
+          const reader = new FileReader();
+          reader.onloadend = () => resolve(reader.result);
+          reader.onerror = err => reject(err);
+          reader.readAsArrayBuffer(file);
+        });
+

[... diff too long, it was truncated ...]

GitHub sha: 59271eb1