DEV: Refactor EncryptedPostCreator

DEV: Refactor EncryptedPostCreator

diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index d210392..2b110c5 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -2,6 +2,7 @@ en:
   encrypt:
     encrypted_excerpt: "This message is encrypted, please read it in the browser."
     enabled_already: "You have already enabled encrypted messages."
+    only_pms: "Only private messages can be encrypted."
   site_settings:
     encrypt_enabled: "Enable encrypted private messages."
     encrypt_groups: "The name of groups that are able to use encryption (empty means everyone)."
diff --git a/lib/encrypted_post_creator.rb b/lib/encrypted_post_creator.rb
new file mode 100644
index 0000000..41c2c70
--- /dev/null
+++ b/lib/encrypted_post_creator.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+class EncryptedPostCreator < PostCreator
+  def initialize(user, opts)
+    super
+  end
+
+  def create
+    if encrypt_valid?
+      topic_key = @opts[:topic_key] || SecureRandom.random_bytes(32)
+
+      # Encrypt title and post contents
+      @opts[:raw] = EncryptedPostCreator.encrypt(@opts[:raw], topic_key)
+      if title = @opts[:title]
+        @opts[:title] = I18n.t("js.encrypt.encrypted_topic_title")
+        @opts[:topic_opts] ||= {}
+        @opts[:topic_opts][:custom_fields] ||= {}
+        @opts[:topic_opts][:custom_fields][DiscourseEncrypt::TITLE_CUSTOM_FIELD] = EncryptedPostCreator.encrypt(title, topic_key)
+      end
+
+      ret = super
+    end
+
+    if @post && errors.blank?
+      # Save the topic key if this is a new topic
+      if !@opts[:topic_key]
+        users.each do |user|
+          key = EncryptedPostCreator.export_key(user, topic_key)
+          DiscourseEncrypt::Store.set("key_#{@post.topic_id}_#{user.id}", key)
+        end
+      end
+    end
+
+    ret
+  end
+
+  def encrypt_valid?
+    @topic = Topic.find_by(id: @opts[:topic_id]) if @opts[:topic_id]
+    if @opts[:archetype] != Archetype.private_message && !@topic&.is_encrypted?
+      errors.add(:base, I18n.t('encrypt.only_pms'))
+      return false
+    end
+
+    users.each do |user|
+      if !user.encrypt_key
+        errors.add(:base, I18n.t('js.encrypt.composer.user_has_no_key', username: user.username))
+        return false
+      end
+    end
+
+    true
+  end
+
+  private
+
+  def users
+    @users ||= begin
+      usernames = @opts[:target_usernames].split(',')
+      users = User.where(username: usernames)
+      User.preload_custom_fields(users, [DiscourseEncrypt::PUBLIC_CUSTOM_FIELD])
+      users
+    end
+  end
+
+  def self.encrypt(raw, key)
+    iv = SecureRandom.random_bytes(12)
+
+    cipher = OpenSSL::Cipher::AES.new(256, :GCM).encrypt
+    cipher.key = key
+    cipher.iv = iv
+    cipher.auth_data = ""
+
+    plaintext = JSON.dump(raw: raw)
+    ciphertext = cipher.update(plaintext)
+    ciphertext += cipher.final
+    ciphertext += cipher.auth_tag
+
+    "1$#{Base64.strict_encode64(iv)}#{Base64.strict_encode64(ciphertext)}"
+  end
+
+  def self.export_key(user, topic_key)
+    Base64.strict_encode64(user.encrypt_key.public_encrypt_oaep256(topic_key))
+  end
+end
diff --git a/lib/post_creator.rb b/lib/post_creator.rb
deleted file mode 100644
index 9b80d7b..0000000
--- a/lib/post_creator.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-
-class PostCreator
-  def create_encrypted
-    topic_key = @opts[:topic_key] || SecureRandom.random_bytes(32)
-
-    @opts[:raw] = PostCreator.encrypted_post(@opts[:raw], topic_key)
-    if title = @opts[:title]
-      @opts[:title] = I18n.t("js.encrypt.encrypted_topic_title")
-      @opts[:topic_opts] ||= {}
-      @opts[:topic_opts][:custom_fields] ||= {}
-      @opts[:topic_opts][:custom_fields][:encrypted_title] = PostCreator.encrypted_post(title, topic_key)
-    end
-
-    return if !create
-
-    names = @opts[:target_usernames].split(',')
-    users = User.where(username: names)
-    User.preload_custom_fields(users, ['encrypt_public'])
-
-    if !@opts[:topic_key]
-      users.each do |user|
-        key = PostCreator.export_key(user, topic_key)
-        DiscourseEncrypt::Store.set("key_#{@post.topic_id}_#{user.id}", key)
-      end
-    end
-
-    @post
-  end
-
-  private
-
-  def self.encrypted_post(raw, key)
-    iv = SecureRandom.random_bytes(12)
-
-    cipher = OpenSSL::Cipher::AES.new(256, :GCM).encrypt
-    cipher.key = key
-    cipher.iv = iv
-    cipher.auth_data = ""
-
-    plaintext = JSON.dump(raw: raw)
-    ciphertext = cipher.update(plaintext)
-    ciphertext += cipher.final
-    ciphertext += cipher.auth_tag
-
-    "1$#{Base64.strict_encode64(iv)}#{Base64.strict_encode64(ciphertext)}"
-  end
-
-  def self.export_key(user, topic_key)
-    identity = user.custom_fields['encrypt_public']
-    jwk = JSON.parse(Base64.decode64(identity[2..-1]))['encryptPublic']
-
-    n = OpenSSL::BN.new(Base64.urlsafe_decode64(jwk['n']), 2)
-    e = OpenSSL::BN.new(Base64.urlsafe_decode64(jwk['e']), 2)
-    user_key = OpenSSL::PKey::RSA.new.tap { |k| k.set_key(n, e, nil) }
-
-    Base64.strict_encode64(user_key.public_encrypt_oaep256(topic_key))
-  end
-end
diff --git a/plugin.rb b/plugin.rb
index cf7dbe3..b234d7f 100644
--- a/plugin.rb
+++ b/plugin.rb
@@ -12,21 +12,20 @@ enabled_site_setting :encrypt_enabled
 register_asset 'stylesheets/common/encrypt.scss'
 %w[exchange-alt far-clipboard file-export lock plus times ticket-alt trash-alt unlock].each { |i| register_svg_icon(i) }
 
-# Register custom user fields to store user's key pair (public and private key)
-# and passphrase salt.
-DiscoursePluginRegistry.serialized_current_user_fields << 'encrypt_public'
-DiscoursePluginRegistry.serialized_current_user_fields << 'encrypt_private'
-
 after_initialize do
   load File.expand_path("../app/jobs/scheduled/encrypt_consistency.rb", __FILE__)
+  load File.expand_path("../lib/encrypted_post_creator.rb", __FILE__)
   load File.expand_path("../lib/openssl.rb", __FILE__)
-  load File.expand_path("../lib/post_creator.rb", __FILE__)
 
   Rails.configuration.filter_parameters << :encrypt_private
 
   module ::DiscourseEncrypt
     PLUGIN_NAME = 'discourse-encrypt'
 
+    PUBLIC_CUSTOM_FIELD = 'encrypt_public'
+    PRIVATE_CUSTOM_FIELD = 'encrypt_private'
+    TITLE_CUSTOM_FIELD = 'encrypted_title'
+
     Store = PluginStore.new(PLUGIN_NAME)
 
     class Engine < ::Rails::Engine
@@ -130,7 +129,7 @@ after_initialize do
         if params[:everything] == 'true'
           TopicAllowedUser
             .joins(topic: :_custom_fields)
-            .where(topic_custom_fields: { name: 'encrypted_title' })
+            .where(topic_custom_fields: { name: DiscourseEncrypt::TITLE_CUSTOM_FIELD })
             .where(topic_allowed_users: { user_id: user.id })
             .delete_all
 
@@ -185,9 +184,12 @@ after_initialize do
     end
   end
 
-  add_preloaded_topic_list_custom_field('encrypted_title')
-  CategoryList.preloaded_topic_custom_fields << 'encrypted_title'
-  Search.preloaded_topic_custom_fields << 'encrypted_title'
+  DiscoursePluginRegistry.serialized_current_user_fields << DiscourseEncrypt::PUBLIC_CUSTOM_FIELD
+  DiscoursePluginRegistry.serialized_current_user_fields << DiscourseEncrypt::PRIVATE_CUSTOM_FIELD
+
+  add_preloaded_topic_list_custom_field(DiscourseEncrypt::TITLE_CUSTOM_FIELD)
+  CategoryList.preloaded_topic_custom_fields << DiscourseEncrypt::TITLE_CUSTOM_FIELD
+  Search.preloaded_topic_custom_fields << DiscourseEncrypt::TITLE_CUSTOM_FIELD
 
   # Hide cooked content.
   Plugin::Filter.register(:after_post_cook) do |post, cooked|
@@ -233,7 +235,7 @@ after_initialize do
     if encrypted_title = manager.args[:encrypted_title]
       manager.args[:topic_opts] ||= {}
       manager.args[:topic_opts][:custom_fields] ||= {}
-      manager.args[:topic_opts][:custom_fields][:encrypted_title] = encrypted_title
+      manager.args[:topic_opts][:custom_fields][DiscourseEncrypt::TITLE_CUSTOM_FIELD] = encrypted_title
     end
 
     if encrypted_raw = manager.args[:encrypted_raw]
@@ -252,11 +254,30 @@ after_initialize do

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

GitHub sha: e2c3637b

1 Like