about summary refs log tree commit diff
path: root/app/models
diff options
context:
space:
mode:
authorThibG <thib@sitedethib.com>2020-06-09 13:00:24 +0200
committerGitHub <noreply@github.com>2020-06-09 13:00:24 +0200
commit3287a10fe9658a6c370431a925abc50d4f68ea08 (patch)
tree88a408a943aa421a6ba844d309f673150cd92434 /app/models
parentf328f2faa3fbdb182921366c6a20e745c069b840 (diff)
parente9d7ca7645fc37534df1222e8d7ab8f3d533b852 (diff)
Merge pull request #1350 from ThibG/glitch-soc/merge-upstream
Merge upstream changes
Diffstat (limited to 'app/models')
-rw-r--r--app/models/account.rb1
-rw-r--r--app/models/concerns/account_associations.rb1
-rw-r--r--app/models/device.rb35
-rw-r--r--app/models/encrypted_message.rb50
-rw-r--r--app/models/message_franking.rb19
-rw-r--r--app/models/one_time_key.rb21
-rw-r--r--app/models/preview_card.rb9
-rw-r--r--app/models/system_key.rb41
-rw-r--r--app/models/user.rb28
9 files changed, 203 insertions, 2 deletions
diff --git a/app/models/account.rb b/app/models/account.rb
index 5038d4768..0b3c48543 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -49,6 +49,7 @@
 #  hide_collections              :boolean
 #  avatar_storage_schema_version :integer
 #  header_storage_schema_version :integer
+#  devices_url                   :string
 #
 
 class Account < ApplicationRecord
diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb
index 499edbf4e..cca3a17fa 100644
--- a/app/models/concerns/account_associations.rb
+++ b/app/models/concerns/account_associations.rb
@@ -9,6 +9,7 @@ module AccountAssociations
 
     # Identity proofs
     has_many :identity_proofs, class_name: 'AccountIdentityProof', dependent: :destroy, inverse_of: :account
+    has_many :devices, dependent: :destroy, inverse_of: :account
 
     # Timelines
     has_many :statuses, inverse_of: :account, dependent: :destroy
diff --git a/app/models/device.rb b/app/models/device.rb
new file mode 100644
index 000000000..97d0d2774
--- /dev/null
+++ b/app/models/device.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: devices
+#
+#  id              :bigint(8)        not null, primary key
+#  access_token_id :bigint(8)
+#  account_id      :bigint(8)
+#  device_id       :string           default(""), not null
+#  name            :string           default(""), not null
+#  fingerprint_key :text             default(""), not null
+#  identity_key    :text             default(""), not null
+#  created_at      :datetime         not null
+#  updated_at      :datetime         not null
+#
+
+class Device < ApplicationRecord
+  belongs_to :access_token, class_name: 'Doorkeeper::AccessToken'
+  belongs_to :account
+
+  has_many :one_time_keys, dependent: :destroy, inverse_of: :device
+  has_many :encrypted_messages, dependent: :destroy, inverse_of: :device
+
+  validates :name, :fingerprint_key, :identity_key, presence: true
+  validates :fingerprint_key, :identity_key, ed25519_key: true
+
+  before_save :invalidate_associations, if: -> { device_id_changed? || fingerprint_key_changed? || identity_key_changed? }
+
+  private
+
+  def invalidate_associations
+    one_time_keys.destroy_all
+    encrypted_messages.destroy_all
+  end
+end
diff --git a/app/models/encrypted_message.rb b/app/models/encrypted_message.rb
new file mode 100644
index 000000000..5e0aba434
--- /dev/null
+++ b/app/models/encrypted_message.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: encrypted_messages
+#
+#  id               :bigint(8)        not null, primary key
+#  device_id        :bigint(8)
+#  from_account_id  :bigint(8)
+#  from_device_id   :string           default(""), not null
+#  type             :integer          default(0), not null
+#  body             :text             default(""), not null
+#  digest           :text             default(""), not null
+#  message_franking :text             default(""), not null
+#  created_at       :datetime         not null
+#  updated_at       :datetime         not null
+#
+
+class EncryptedMessage < ApplicationRecord
+  self.inheritance_column = nil
+
+  include Paginable
+
+  scope :up_to, ->(id) { where(arel_table[:id].lteq(id)) }
+
+  belongs_to :device
+  belongs_to :from_account, class_name: 'Account'
+
+  around_create Mastodon::Snowflake::Callbacks
+
+  after_commit :push_to_streaming_api
+
+  private
+
+  def push_to_streaming_api
+    Rails.logger.info(streaming_channel)
+    Rails.logger.info(subscribed_to_timeline?)
+
+    return if destroyed? || !subscribed_to_timeline?
+
+    PushEncryptedMessageWorker.perform_async(id)
+  end
+
+  def subscribed_to_timeline?
+    Redis.current.exists("subscribed:#{streaming_channel}")
+  end
+
+  def streaming_channel
+    "timeline:#{device.account_id}:#{device.device_id}"
+  end
+end
diff --git a/app/models/message_franking.rb b/app/models/message_franking.rb
new file mode 100644
index 000000000..c72bd1cca
--- /dev/null
+++ b/app/models/message_franking.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class MessageFranking
+  attr_reader :hmac, :source_account_id, :target_account_id,
+              :timestamp, :original_franking
+
+  def initialize(attributes = {})
+    @hmac              = attributes[:hmac]
+    @source_account_id = attributes[:source_account_id]
+    @target_account_id = attributes[:target_account_id]
+    @timestamp         = attributes[:timestamp]
+    @original_franking = attributes[:original_franking]
+  end
+
+  def to_token
+    crypt = ActiveSupport::MessageEncryptor.new(SystemKey.current_key, serializer: Oj)
+    crypt.encrypt_and_sign(self)
+  end
+end
diff --git a/app/models/one_time_key.rb b/app/models/one_time_key.rb
new file mode 100644
index 000000000..8ada34824
--- /dev/null
+++ b/app/models/one_time_key.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: one_time_keys
+#
+#  id         :bigint(8)        not null, primary key
+#  device_id  :bigint(8)
+#  key_id     :string           default(""), not null
+#  key        :text             default(""), not null
+#  signature  :text             default(""), not null
+#  created_at :datetime         not null
+#  updated_at :datetime         not null
+#
+
+class OneTimeKey < ApplicationRecord
+  belongs_to :device
+
+  validates :key_id, :key, :signature, presence: true
+  validates :key, ed25519_key: true
+  validates :signature, ed25519_signature: { message: :key, verify_key: ->(one_time_key) { one_time_key.device.fingerprint_key } }
+end
diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb
index 2802f4667..235928260 100644
--- a/app/models/preview_card.rb
+++ b/app/models/preview_card.rb
@@ -23,19 +23,25 @@
 #  updated_at                   :datetime         not null
 #  embed_url                    :string           default(""), not null
 #  image_storage_schema_version :integer
+#  blurhash                     :string
 #
 
 class PreviewCard < ApplicationRecord
   IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
   LIMIT = 1.megabytes
 
+  BLURHASH_OPTIONS = {
+    x_comp: 4,
+    y_comp: 4,
+  }.freeze
+
   self.inheritance_column = false
 
   enum type: [:link, :photo, :video, :rich]
 
   has_and_belongs_to_many :statuses
 
-  has_attached_file :image, styles: ->(f) { image_styles(f) }, convert_options: { all: '-quality 80 -strip' }
+  has_attached_file :image, processors: [:thumbnail, :blurhash_transcoder], styles: ->(f) { image_styles(f) }, convert_options: { all: '-quality 80 -strip' }
 
   include Attachmentable
 
@@ -72,6 +78,7 @@ class PreviewCard < ApplicationRecord
           geometry: '400x400>',
           file_geometry_parser: FastGeometryParser,
           convert_options: '-coalesce -strip',
+          blurhash: BLURHASH_OPTIONS,
         },
       }
 
diff --git a/app/models/system_key.rb b/app/models/system_key.rb
new file mode 100644
index 000000000..f17db7c2d
--- /dev/null
+++ b/app/models/system_key.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: system_keys
+#
+#  id         :bigint(8)        not null, primary key
+#  key        :binary
+#  created_at :datetime         not null
+#  updated_at :datetime         not null
+#
+class SystemKey < ApplicationRecord
+  ROTATION_PERIOD = 1.week.freeze
+
+  before_validation :set_key
+
+  scope :expired, ->(now = Time.now.utc) { where(arel_table[:created_at].lt(now - ROTATION_PERIOD * 3)) }
+
+  class << self
+    def current_key
+      previous_key = order(id: :asc).last
+
+      if previous_key && previous_key.created_at >= ROTATION_PERIOD.ago
+        previous_key.key
+      else
+        create.key
+      end
+    end
+  end
+
+  private
+
+  def set_key
+    return if key.present?
+
+    cipher = OpenSSL::Cipher.new('AES-256-GCM')
+    cipher.encrypt
+
+    self.key = cipher.random_key
+  end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index c8dbd2fd3..a05d98d88 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -38,6 +38,8 @@
 #  chosen_languages          :string           is an Array
 #  created_by_application_id :bigint(8)
 #  approved                  :boolean          default(TRUE), not null
+#  sign_in_token             :string
+#  sign_in_token_sent_at     :datetime
 #
 
 class User < ApplicationRecord
@@ -114,7 +116,7 @@ class User < ApplicationRecord
            :default_content_type, :system_emoji_font,
            to: :settings, prefix: :setting, allow_nil: false
 
-  attr_reader :invite_code
+  attr_reader :invite_code, :sign_in_token_attempt
   attr_writer :external
 
   def confirmed?
@@ -168,6 +170,10 @@ class User < ApplicationRecord
     true
   end
 
+  def suspicious_sign_in?(ip)
+    !otp_required_for_login? && current_sign_in_at.present? && current_sign_in_at < 2.weeks.ago && !recent_ip?(ip)
+  end
+
   def functional?
     confirmed? && approved? && !disabled? && !account.suspended?
   end
@@ -270,6 +276,13 @@ class User < ApplicationRecord
     super
   end
 
+  def external_or_valid_password?(compare_password)
+    # If encrypted_password is blank, we got the user from LDAP or PAM,
+    # so credentials are already valid
+
+    encrypted_password.blank? || valid_password?(compare_password)
+  end
+
   def send_reset_password_instructions
     return false if encrypted_password.blank?
 
@@ -305,6 +318,15 @@ class User < ApplicationRecord
     end
   end
 
+  def sign_in_token_expired?
+    sign_in_token_sent_at.nil? || sign_in_token_sent_at < 5.minutes.ago
+  end
+
+  def generate_sign_in_token
+    self.sign_in_token         = Devise.friendly_token(6)
+    self.sign_in_token_sent_at = Time.now.utc
+  end
+
   protected
 
   def send_devise_notification(notification, *args)
@@ -321,6 +343,10 @@ class User < ApplicationRecord
 
   private
 
+  def recent_ip?(ip)
+    recent_ips.any? { |(_, recent_ip)| recent_ip == ip }
+  end
+
   def send_pending_devise_notifications
     pending_devise_notifications.each do |notification, args|
       render_and_send_devise_message(notification, *args)