DEV: Implement encrypt on the server side

DEV: Implement encrypt on the server side

This can be useful for automated messages.

diff --git a/lib/open_ssl.rb b/lib/open_ssl.rb
new file mode 100644
index 0000000..86b5c50
--- /dev/null
+++ b/lib/open_ssl.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'openssl'
+require 'securerandom'
+
+module OpenSSL
+  module PKey
+    class RSA
+      def public_encrypt_oaep256(msg)
+        public_encrypt(PKCS1.oaep_mgf1(msg, n.num_bytes), OpenSSL::PKey::RSA::NO_PADDING)
+      end
+    end
+  end
+
+  module PKCS1
+    # Public-Key Cryptography Standards (PKCS) #1: RSA Cryptography
+    # Page 18, https://www.ietf.org/rfc/rfc3447
+    def oaep_mgf1(msg, k)
+      m_len = msg.bytesize
+      h_len = OpenSSL::Digest::SHA256.new.digest_length
+      raise OpenSSL::PKey::RSAError, 'message too long' if m_len > k - 2 * h_len - 2
+
+      l_hash = OpenSSL::Digest::SHA256.digest('') # label = ''
+      ps = [0] * (k - m_len - 2 * h_len - 2)
+      db = l_hash + ps.pack('C*') + [1].pack('C') + [msg].pack('a*')
+      seed = SecureRandom.random_bytes(h_len)
+      db_mask = mgf1(seed, k - h_len - 1)
+      masked_db = db.bytes.zip(db_mask).map! { |a, b| a ^ b }.pack('C*')
+      seed_mask = mgf1(masked_db, h_len)
+      masked_seed = seed.bytes.zip(seed_mask).map! { |a, b| a ^ b }.pack('C*')
+      [0, masked_seed, masked_db].pack('Ca*a*')
+    end
+
+    module_function :oaep_mgf1
+
+    # Public-Key Cryptography Standards (PKCS) #1: RSA Cryptography
+    # Page 54, https://www.ietf.org/rfc/rfc3447
+    def mgf1(seed, mask_len)
+      c = 0
+      t = []
+      while t.size < mask_len
+        t += OpenSSL::Digest::SHA256.digest([seed, c].pack('a*N')).bytes
+        c += 1
+      end
+      t[0..mask_len]
+    end
+
+    module_function :mgf1
+  end
+end
diff --git a/lib/post_creator.rb b/lib/post_creator.rb
new file mode 100644
index 0000000..8995275
--- /dev/null
+++ b/lib/post_creator.rb
@@ -0,0 +1,59 @@
+# 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..]))['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 5a4a4d4..6b25163 100644
--- a/plugin.rb
+++ b/plugin.rb
@@ -19,6 +19,8 @@ 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/open_ssl.rb", __FILE__)
+  load File.expand_path("../lib/post_creator.rb", __FILE__)
 
   Rails.configuration.filter_parameters << :encrypt_private

GitHub sha: a4a96742

1 Like

DEV: Fix build