Major improvements, refactorings and some bug fixes.

Major improvements, refactorings and some bug fixes.

  • Refactored identity and key management logic.

  • Introduced protocol v1 which comes with integrity checks for the posts and improved encoding.

diff --git a/app/jobs/encrypt_consistency.rb b/app/jobs/encrypt_consistency.rb
new file mode 100644
index 0000000..b260f8a
--- /dev/null
+++ b/app/jobs/encrypt_consistency.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Jobs
+  class VotingEnsureConsistency < Jobs::Scheduled
+    every 1.day
+
+    def execute(args)
+      DB.query(<<~SQL
+          SELECT taf.user_id, taf.topic_id
+          FROM topic_allowed_users taf
+          JOIN topic_custom_fields tcf ON taf.topic_id = tcf.topic_id AND tcf.name = 'encrypted_title'
+          WHERE 'key_' || taf.topic_id || '_' || taf.user_id NOT IN
+            (SELECT key
+            FROM plugin_store_rows
+            WHERE plugin_name = 'discourse-encrypt' AND key LIKE 'key_%')
+        SQL
+      ).each do |row|
+        Discourse.warn('User was invited to encrypted topic, but has no topic key.', user_id: row.user_id, topic_id: row.topic_id)
+        TopicAllowedUser.find_by(user_id: row.user_id, topic_id: row.topic_id).delete
+      end
+
+      DB.query(<<~SQL
+          WITH encrypt_keys AS (
+            SELECT key, split_part(key, '_', 2)::INTEGER AS topic_id, split_part(key, '_', 3)::INTEGER AS user_id
+            FROM plugin_store_rows
+            WHERE plugin_name = 'discourse-encrypt' AND key LIKE 'key_%'
+          )
+          SELECT ek.user_id, ek.topic_id
+          FROM encrypt_keys ek
+          LEFT JOIN topic_allowed_users taf ON ek.topic_id = taf.topic_id AND ek.user_id = taf.user_id
+          WHERE taf.id IS NULL
+        SQL
+      ).each do |row|
+        Discourse.warn('User has topic key, but was not invited to topic.', user_id: row.user_id, topic_id: row.topic_id)
+        DiscourseEncrypt::Store.remove("key_#{row.topic_id}_#{row.user_id}")
+      end
+    end
+  end
+end
diff --git a/assets/javascripts/discourse/connectors/user-preferences-account/encrypt.js.es6 b/assets/javascripts/discourse/connectors/user-preferences-account/encrypt.js.es6
index b7eceee..b45e256 100644
--- a/assets/javascripts/discourse/connectors/user-preferences-account/encrypt.js.es6
+++ b/assets/javascripts/discourse/connectors/user-preferences-account/encrypt.js.es6
@@ -1,30 +1,22 @@
+import { registerHelper } from "discourse-common/lib/helpers";
 import { ajax } from "discourse/lib/ajax";
 import { popupAjaxError } from "discourse/lib/ajax-error";
 import showModal from "discourse/lib/show-modal";
-import { registerHelper } from "discourse-common/lib/helpers";
-import {
-  exportPrivateKey,
-  exportPublicKey,
-  generateKeyPair,
-  generatePassphraseKey,
-  generateSalt,
-  importPrivateKey,
-  importPublicKey
-} from "discourse/plugins/discourse-encrypt/lib/keys";
 import {
-  saveKeyPairToIndexedDb,
-  deleteIndexedDb
-} from "discourse/plugins/discourse-encrypt/lib/keys_db";
+  deleteDb,
+  saveDbIdentity
+} from "discourse/plugins/discourse-encrypt/lib/database";
 import {
   canEnableEncrypt,
   ENCRYPT_ACTIVE,
   ENCRYPT_DISABLED,
-  getEncryptionStatus,
-  PACKED_KEY_FOOTER,
-  PACKED_KEY_HEADER,
-  PACKED_KEY_SEPARATOR,
-  reload
+  getEncryptionStatus
 } from "discourse/plugins/discourse-encrypt/lib/discourse";
+import {
+  exportIdentity,
+  generateIdentity,
+  importIdentity
+} from "discourse/plugins/discourse-encrypt/lib/protocol";
 
 // TODO: I believe this should get into core.
 // Handlebars offers `if` but no other helpers for conditions, which eventually
@@ -126,126 +118,57 @@ export default {
     enableEncrypt() {
       this.set("inProgress", true);
 
-      // 1. Generate new key pair or import existing one.
-      let keyPairPromise;
-      if (this.importKey) {
-        const str = (this.key || PACKED_KEY_SEPARATOR)
-          .replace(PACKED_KEY_HEADER, "")
-          .replace(PACKED_KEY_FOOTER, "")
-          .split(PACKED_KEY_SEPARATOR);
-
-        const publicStr = str[0]
-          .split(/\s+/)
-          .map(x => x.trim())
-          .join("");
-        const privateStr = str[1]
-          .split(/\s+/)
-          .map(x => x.trim())
-          .join("");
-
-        keyPairPromise = Ember.RSVP.Promise.all([
-          importPublicKey(publicStr),
-          importPublicKey(privateStr, ["decrypt", "unwrapKey"])
-        ]);
-      } else {
-        keyPairPromise = generateKeyPair();
-      }
-
-      // 2. a. Export public key to string.
-      // 2. b. Export private key to a string (using passphrase).
-      keyPairPromise
-        .then(keyPair => {
-          const [publicKey, privateKey] = keyPair;
-
-          const passphrase = this.passphrase;
-          const salt = generateSalt();
-          const publicStr = exportPublicKey(publicKey);
-          const privateStr = generatePassphraseKey(passphrase, salt).then(
-            passphraseKey => exportPrivateKey(privateKey, passphraseKey)
-          );
-
-          return Ember.RSVP.Promise.all([publicStr, privateStr, salt]);
-        })
+      const identityPromise = this.importKey
+        ? importIdentity(this.key)
+        : generateIdentity();
 
-        // 3. Save keys to server.
-        .then(([publicStr, privateStr, salt]) => {
-          this.set("model.custom_fields.encrypt_public_key", publicStr);
-          this.set("model.custom_fields.encrypt_private_key", privateStr);
-          this.set("model.custom_fields.encrypt_salt", salt);
-          const saveKeys = ajax("/encrypt/keys", {
+      const saveIdentityPromise = identityPromise
+        .then(identity => exportIdentity(identity, this.passphrase))
+        .then(exported => {
+          this.set("model.custom_fields.encrypt_public", exported.public);
+          this.set("model.custom_fields.encrypt_private", exported.private);
+          return ajax("/encrypt/keys", {
             type: "PUT",
-            data: { public_key: publicStr, private_key: privateStr, salt }
+            data: {
+              public: exported.public,
+              private: exported.private
+            }
           });
-
-          return Ember.RSVP.Promise.all([
-            publicStr,
-            privateStr,
-            salt,
-            saveKeys
-          ]);
-        })
-
-        // 4. Re-import keys but this time as `unextractable`.
-        .then(([publicStr, privateStr, salt]) =>
-          Ember.RSVP.Promise.all([
-            importPublicKey(publicStr),
-            generatePassphraseKey(this.passphrase, salt).then(passphraseKey =>
-              importPrivateKey(privateStr, passphraseKey)
-            )
-          ])
-        )
-
-        // 5. Save key pair in local IndexedDb.
-        .then(([publicKey, privateKey]) =>
-          saveKeyPairToIndexedDb(publicKey, privateKey)
-        )
-
-        // 6. Reset component status.
+        });
+
+      return Ember.RSVP.Promise.all([
+        identityPromise
+          .then(identity => exportIdentity(identity))
+          .then(exported => importIdentity(exported)),
+        saveIdentityPromise
+      ])
+        .then(results => saveDbIdentity(results[0]))
         .then(() => {
           this.appEvents.trigger("encrypt:status-changed");
-
+          window.location.reload();
+        })
+        .catch(popupAjaxError)
+        .finally(() => {
           this.send("hidePassphraseInput");
           this.setProperties({
             inProgress: false,
             importKey: false,
             key: ""
           });
-
-          reload();
-        })
-
-        .catch(popupAjaxError);
+        });
     },
 
     activateEncrypt() {
       this.set("inProgress", true);
 
-      const publicStr = this.get("model.custom_fields.encrypt_public_key");
-      const privateStr = this.get("model.custom_fields.encrypt_private_key");
-      const salt = this.get("model.custom_fields.encrypt_salt");
-      const passphrase = this.passphrase;
-
-      // 1. a. Import public key from string.
-      // 1. b. Import private from string (using passphrase).
-      const importPub = importPublicKey(publicStr);
-      const importPrv = generatePassphraseKey(passphrase, salt).then(
-        passphraseKey => importPrivateKey(privateStr, passphraseKey)

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

GitHub sha: 09b49556

1 Like