Conflicts: - `app/controllers/activitypub/collections_controller.rb`: Conflict due to glitch-soc having to take care of local-only pinned toots in that controller. Took upstream's changes and restored the local-only special handling. - `app/controllers/auth/sessions_controller.rb`: Minor conflicts due to the theming system, applied upstream changes, adapted the following two files for glitch-soc's theming system: - `app/controllers/concerns/sign_in_token_authentication_concern.rb` - `app/controllers/concerns/two_factor_authentication_concern.rb` - `app/services/backup_service.rb`: Minor conflict due to glitch-soc having to handle local-only toots specially. Applied upstream changes and restored the local-only special handling. - `app/views/admin/custom_emojis/index.html.haml`: Minor conflict due to the theming system. - `package.json`: Upstream dependency updated, too close to a glitch-soc-only dependency in the file. - `yarn.lock`: Upstream dependency updated, too close to a glitch-soc-only dependency in the file.main
commit
12c8ac9e14
@ -1,28 +0,0 @@
|
|||||||
version: 1
|
|
||||||
|
|
||||||
update_configs:
|
|
||||||
- package_manager: "ruby:bundler"
|
|
||||||
directory: "/"
|
|
||||||
update_schedule: "weekly"
|
|
||||||
# Supported update schedule: live daily weekly monthly
|
|
||||||
version_requirement_updates: "auto"
|
|
||||||
# Supported version requirements: auto widen_ranges increase_versions increase_versions_if_necessary
|
|
||||||
allowed_updates:
|
|
||||||
- match:
|
|
||||||
dependency_type: "all"
|
|
||||||
# Supported dependency types: all indirect direct production development
|
|
||||||
update_type: "all"
|
|
||||||
# Supported update types: all security
|
|
||||||
|
|
||||||
- package_manager: "javascript"
|
|
||||||
directory: "/"
|
|
||||||
update_schedule: "weekly"
|
|
||||||
# Supported update schedule: live daily weekly monthly
|
|
||||||
version_requirement_updates: "auto"
|
|
||||||
# Supported version requirements: auto widen_ranges increase_versions increase_versions_if_necessary
|
|
||||||
allowed_updates:
|
|
||||||
- match:
|
|
||||||
dependency_type: "all"
|
|
||||||
# Supported dependency types: all indirect direct production development
|
|
||||||
update_type: "all"
|
|
||||||
# Supported update types: all security
|
|
@ -0,0 +1,22 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
open-pull-requests-limit: 99
|
||||||
|
allow:
|
||||||
|
- dependency-type: all
|
||||||
|
|
||||||
|
- package-ecosystem: bundler
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
open-pull-requests-limit: 99
|
||||||
|
allow:
|
||||||
|
- dependency-type: all
|
@ -0,0 +1,12 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 3.1.x | :white_check_mark: |
|
||||||
|
| < 3.1 | :x: |
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
hello@joinmastodon.org
|
@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::ClaimsController < ActivityPub::BaseController
|
||||||
|
include SignatureVerification
|
||||||
|
include AccountOwnedConcern
|
||||||
|
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
|
||||||
|
before_action :require_signature!
|
||||||
|
before_action :set_claim_result
|
||||||
|
|
||||||
|
def create
|
||||||
|
render json: @claim_result, serializer: ActivityPub::OneTimeKeySerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_claim_result
|
||||||
|
@claim_result = ::Keys::ClaimService.new.call(@account.id, params[:id])
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,30 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Crypto::DeliveriesController < Api::BaseController
|
||||||
|
before_action -> { doorkeeper_authorize! :crypto }
|
||||||
|
before_action :require_user!
|
||||||
|
before_action :set_current_device
|
||||||
|
|
||||||
|
def create
|
||||||
|
devices.each do |device_params|
|
||||||
|
DeliverToDeviceService.new.call(current_account, @current_device, device_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_current_device
|
||||||
|
@current_device = Device.find_by!(access_token: doorkeeper_token)
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params.require(:device)
|
||||||
|
params.permit(device: [:account_id, :device_id, :type, :body, :hmac])
|
||||||
|
end
|
||||||
|
|
||||||
|
def devices
|
||||||
|
Array(resource_params[:device])
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,59 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Crypto::EncryptedMessagesController < Api::BaseController
|
||||||
|
LIMIT = 80
|
||||||
|
|
||||||
|
before_action -> { doorkeeper_authorize! :crypto }
|
||||||
|
before_action :require_user!
|
||||||
|
before_action :set_current_device
|
||||||
|
|
||||||
|
before_action :set_encrypted_messages, only: :index
|
||||||
|
after_action :insert_pagination_headers, only: :index
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: @encrypted_messages, each_serializer: REST::EncryptedMessageSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear
|
||||||
|
@current_device.encrypted_messages.up_to(params[:up_to_id]).delete_all
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_current_device
|
||||||
|
@current_device = Device.find_by!(access_token: doorkeeper_token)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_encrypted_messages
|
||||||
|
@encrypted_messages = @current_device.encrypted_messages.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
def insert_pagination_headers
|
||||||
|
set_pagination_headers(next_path, prev_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_path
|
||||||
|
api_v1_crypto_encrypted_messages_url pagination_params(max_id: pagination_max_id) if records_continue?
|
||||||
|
end
|
||||||
|
|
||||||
|
def prev_path
|
||||||
|
api_v1_crypto_encrypted_messages_url pagination_params(min_id: pagination_since_id) unless @encrypted_messages.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_max_id
|
||||||
|
@encrypted_messages.last.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_since_id
|
||||||
|
@encrypted_messages.first.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def records_continue?
|
||||||
|
@encrypted_messages.size == limit_param(LIMIT)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_params(core_params)
|
||||||
|
params.slice(:limit).permit(:limit).merge(core_params)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,25 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Crypto::Keys::ClaimsController < Api::BaseController
|
||||||
|
before_action -> { doorkeeper_authorize! :crypto }
|
||||||
|
before_action :require_user!
|
||||||
|
before_action :set_claim_results
|
||||||
|
|
||||||
|
def create
|
||||||
|
render json: @claim_results, each_serializer: REST::Keys::ClaimResultSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_claim_results
|
||||||
|
@claim_results = devices.map { |device_params| ::Keys::ClaimService.new.call(current_account, device_params[:account_id], device_params[:device_id]) }.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params.permit(device: [:account_id, :device_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def devices
|
||||||
|
Array(resource_params[:device])
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Crypto::Keys::CountsController < Api::BaseController
|
||||||
|
before_action -> { doorkeeper_authorize! :crypto }
|
||||||
|
before_action :require_user!
|
||||||
|
before_action :set_current_device
|
||||||
|
|
||||||
|
def show
|
||||||
|
render json: { one_time_keys: @current_device.one_time_keys.count }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_current_device
|
||||||
|
@current_device = Device.find_by!(access_token: doorkeeper_token)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,26 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Crypto::Keys::QueriesController < Api::BaseController
|
||||||
|
before_action -> { doorkeeper_authorize! :crypto }
|
||||||
|
before_action :require_user!
|
||||||
|
before_action :set_accounts
|
||||||
|
before_action :set_query_results
|
||||||
|
|
||||||
|
def create
|
||||||
|
render json: @query_results, each_serializer: REST::Keys::QueryResultSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_accounts
|
||||||
|
@accounts = Account.where(id: account_ids).includes(:devices)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_query_results
|
||||||
|
@query_results = @accounts.map { |account| ::Keys::QueryService.new.call(account) }.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_ids
|
||||||
|
Array(params[:id]).map(&:to_i)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,29 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Crypto::Keys::UploadsController < Api::BaseController
|
||||||
|
before_action -> { doorkeeper_authorize! :crypto }
|
||||||
|
before_action :require_user!
|
||||||
|
|
||||||
|
def create
|
||||||
|
device = Device.find_or_initialize_by(access_token: doorkeeper_token)
|
||||||
|
|
||||||
|
device.transaction do
|
||||||
|
device.account = current_account
|
||||||
|
device.update!(resource_params[:device])
|
||||||
|
|
||||||
|
if resource_params[:one_time_keys].present? && resource_params[:one_time_keys].is_a?(Enumerable)
|
||||||
|
resource_params[:one_time_keys].each do |one_time_key_params|
|
||||||
|
device.one_time_keys.create!(one_time_key_params)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: device, serializer: REST::Keys::DeviceSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params.permit(device: [:device_id, :name, :fingerprint_key, :identity_key], one_time_keys: [:key_id, :key, :signature])
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,50 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module SignInTokenAuthenticationConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
prepend_before_action :authenticate_with_sign_in_token, if: :sign_in_token_required?, only: [:create]
|
||||||
|
end
|
||||||
|
|
||||||
|
def sign_in_token_required?
|
||||||
|
find_user&.suspicious_sign_in?(request.remote_ip)
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_sign_in_token_attempt?(user)
|
||||||
|
Devise.secure_compare(user.sign_in_token, user_params[:sign_in_token_attempt])
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate_with_sign_in_token
|
||||||
|
user = self.resource = find_user
|
||||||
|
|
||||||
|
if user_params[:sign_in_token_attempt].present? && session[:attempt_user_id]
|
||||||
|
authenticate_with_sign_in_token_attempt(user)
|
||||||
|
elsif user.present? && user.external_or_valid_password?(user_params[:password])
|
||||||
|
prompt_for_sign_in_token(user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate_with_sign_in_token_attempt(user)
|
||||||
|
if valid_sign_in_token_attempt?(user)
|
||||||
|
session.delete(:attempt_user_id)
|
||||||
|
remember_me(user)
|
||||||
|
sign_in(user)
|
||||||
|
else
|
||||||
|
flash.now[:alert] = I18n.t('users.invalid_sign_in_token')
|
||||||
|
prompt_for_sign_in_token(user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def prompt_for_sign_in_token(user)
|
||||||
|
if user.sign_in_token_expired?
|
||||||
|
user.generate_sign_in_token && user.save
|
||||||
|
UserMailer.sign_in_token(user, request.remote_ip, request.user_agent, Time.now.utc.to_s).deliver_later!
|
||||||
|
end
|
||||||
|
|
||||||
|
session[:attempt_user_id] = user.id
|
||||||
|
use_pack 'auth'
|
||||||
|
@body_classes = 'lighter'
|
||||||
|
render :sign_in_token
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,48 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module TwoFactorAuthenticationConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
|
||||||
|
end
|
||||||
|
|
||||||
|
def two_factor_enabled?
|
||||||
|
find_user&.otp_required_for_login?
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_otp_attempt?(user)
|
||||||
|
user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
|
||||||
|
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
|
||||||
|
rescue OpenSSL::Cipher::CipherError
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate_with_two_factor
|
||||||
|
user = self.resource = find_user
|
||||||
|
|
||||||
|
if user_params[:otp_attempt].present? && session[:attempt_user_id]
|
||||||
|
authenticate_with_two_factor_attempt(user)
|
||||||
|
elsif user.present? && user.external_or_valid_password?(user_params[:password])
|
||||||
|
prompt_for_two_factor(user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate_with_two_factor_attempt(user)
|
||||||
|
if valid_otp_attempt?(user)
|
||||||
|
session.delete(:attempt_user_id)
|
||||||
|
remember_me(user)
|
||||||
|
sign_in(user)
|
||||||
|
else
|
||||||
|
flash.now[:alert] = I18n.t('users.invalid_otp_token')
|
||||||
|
prompt_for_two_factor(user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def prompt_for_two_factor(user)
|
||||||
|
session[:attempt_user_id] = user.id
|
||||||
|
use_pack 'auth'
|
||||||
|
@body_classes = 'lighter'
|
||||||
|
render :two_factor
|
||||||
|
end
|
||||||
|
end
|
@ -1,14 +1,13 @@
|
|||||||
$black-emojis: '8ball' 'ant' 'back' 'black_circle' 'black_heart' 'black_large_square' 'black_medium_small_square' 'black_medium_square' 'black_nib' 'black_small_square' 'bomb' 'bowling' 'bust_in_silhouette' 'busts_in_silhouette' 'camera' 'camera_with_flash' 'clubs' 'copyright' 'curly_loop' 'currency_exchange' 'dark_sunglasses' 'eight_pointed_black_star' 'electric_plug' 'end' 'female-guard' 'film_projector' 'fried_egg' 'gorilla' 'guardsman' 'heavy_check_mark' 'heavy_division_sign' 'heavy_dollar_sign' 'heavy_minus_sign' 'heavy_multiplication_x' 'heavy_plus_sign' 'hocho' 'hole' 'joystick' 'kaaba' 'lower_left_ballpoint_pen' 'lower_left_fountain_pen' 'male-guard' 'microphone' 'mortar_board' 'movie_camera' 'musical_score' 'on' 'registered' 'soon' 'spades' 'speaking_head_in_silhouette' 'spider' 'telephone_receiver' 'tm' 'top' 'tophat' 'turkey' 'vhs' 'video_camera' 'video_game' 'water_buffalo' 'waving_black_flag' 'wavy_dash';
|
$emojis-requiring-inversion: 'back' 'copyright' 'curly_loop' 'currency_exchange' 'end' 'heavy_check_mark' 'heavy_division_sign' 'heavy_dollar_sign' 'heavy_minus_sign' 'heavy_multiplication_x' 'heavy_plus_sign' 'on' 'registered' 'soon' 'spider' 'telephone_receiver' 'tm' 'top' 'wavy_dash' !default;
|
||||||
|
|
||||||
%white-emoji-outline {
|
%emoji-color-inversion {
|
||||||
filter: drop-shadow(1px 1px 0 $white) drop-shadow(-1px 1px 0 $white) drop-shadow(1px -1px 0 $white) drop-shadow(-1px -1px 0 $white);
|
filter: invert(1);
|
||||||
transform: scale(.71);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.emojione {
|
.emojione {
|
||||||
@each $emoji in $black-emojis {
|
@each $emoji in $emojis-requiring-inversion {
|
||||||
&[title=':#{$emoji}:'] {
|
&[title=':#{$emoji}:'] {
|
||||||
@extend %white-emoji-outline;
|
@extend %emoji-color-inversion;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -0,0 +1,41 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::ActivityPresenter < ActiveModelSerializers::Model
|
||||||
|
attributes :id, :type, :actor, :published, :to, :cc, :virtual_object
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def from_status(status)
|
||||||
|
new.tap do |presenter|
|
||||||
|
presenter.id = ActivityPub::TagManager.instance.activity_uri_for(status)
|
||||||
|
presenter.type = status.reblog? ? 'Announce' : 'Create'
|
||||||
|
presenter.actor = ActivityPub::TagManager.instance.uri_for(status.account)
|
||||||
|
presenter.published = status.created_at
|
||||||
|
presenter.to = ActivityPub::TagManager.instance.to(status)
|
||||||
|
presenter.cc = ActivityPub::TagManager.instance.cc(status)
|
||||||
|
|
||||||
|
presenter.virtual_object = begin
|
||||||
|
if status.reblog?
|
||||||
|
if status.account == status.proper.account && status.proper.private_visibility? && status.local?
|
||||||
|
status.proper
|
||||||
|
else
|
||||||
|
ActivityPub::TagManager.instance.uri_for(status.proper)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
status.proper
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def from_encrypted_message(encrypted_message)
|
||||||
|
new.tap do |presenter|
|
||||||
|
presenter.id = ActivityPub::TagManager.instance.generate_uri_for(nil)
|
||||||
|
presenter.type = 'Create'
|
||||||
|
presenter.actor = ActivityPub::TagManager.instance.uri_for(encrypted_message.source_account)
|
||||||
|
presenter.published = Time.now.utc
|
||||||
|
presenter.to = ActivityPub::TagManager.instance.uri_for(encrypted_message.target_account)
|
||||||
|
presenter.virtual_object = encrypted_message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -1,52 +1,22 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ActivityPub::ActivitySerializer < ActivityPub::Serializer
|
class ActivityPub::ActivitySerializer < ActivityPub::Serializer
|
||||||
attributes :id, :type, :actor, :published, :to, :cc
|
def self.serializer_for(model, options)
|
||||||
|
case model.class.name
|
||||||
has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, if: :serialize_object?
|
when 'Status'
|
||||||
|
ActivityPub::NoteSerializer
|
||||||
attribute :proper_uri, key: :object, unless: :serialize_object?
|
when 'DeliverToDeviceService::EncryptedMessage'
|
||||||
attribute :atom_uri, if: :announce?
|
ActivityPub::EncryptedMessageSerializer
|
||||||
|
else
|
||||||
def id
|
super
|
||||||
ActivityPub::TagManager.instance.activity_uri_for(object)
|
|
||||||
end
|
|
||||||
|
|
||||||
def type
|
|
||||||
announce? ? 'Announce' : 'Create'
|
|
||||||
end
|
|
||||||
|
|
||||||
def actor
|
|
||||||
ActivityPub::TagManager.instance.uri_for(object.account)
|
|
||||||
end
|
|
||||||
|
|
||||||
def published
|
|
||||||
object.created_at.iso8601
|
|
||||||
end
|
|
||||||
|
|
||||||
def to
|
|
||||||
ActivityPub::TagManager.instance.to(object)
|
|
||||||
end
|
|
||||||
|
|
||||||
def cc
|
|
||||||
ActivityPub::TagManager.instance.cc(object)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def proper_uri
|
|
||||||
ActivityPub::TagManager.instance.uri_for(object.proper)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def atom_uri
|
attributes :id, :type, :actor, :published, :to, :cc
|
||||||
OStatus::TagManager.instance.uri_for(object)
|
|
||||||
end
|
|
||||||
|
|
||||||
def announce?
|
has_one :virtual_object, key: :object
|
||||||
object.reblog?
|
|
||||||
end
|
|
||||||
|
|
||||||
def serialize_object?
|
def published
|
||||||
return true unless announce?
|
object.published.iso8601
|
||||||
# Serialize private self-boosts of local toots
|
|
||||||
object.account == object.proper.account && object.proper.private_visibility? && object.local?
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::DeviceSerializer < ActivityPub::Serializer
|
||||||
|
context_extensions :olm
|
||||||
|
|
||||||
|
include RoutingHelper
|
||||||
|
|
||||||
|
class FingerprintKeySerializer < ActivityPub::Serializer
|
||||||
|
attributes :type, :public_key_base64
|
||||||
|
|
||||||
|
def type
|
||||||
|
'Ed25519Key'
|
||||||
|
end
|
||||||
|
|
||||||
|
def public_key_base64
|
||||||
|
object.fingerprint_key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class IdentityKeySerializer < ActivityPub::Serializer
|
||||||
|
attributes :type, :public_key_base64
|
||||||
|
|
||||||
|
def type
|
||||||
|
'Curve25519Key'
|
||||||
|
end
|
||||||
|
|
||||||
|
def public_key_base64
|
||||||
|
object.identity_key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attributes :device_id, :type, :name, :claim
|
||||||
|
|
||||||
|
has_one :fingerprint_key, serializer: FingerprintKeySerializer
|
||||||
|
has_one :identity_key, serializer: IdentityKeySerializer
|
||||||
|
|
||||||
|
def type
|
||||||
|
'Device'
|
||||||
|
end
|
||||||
|
|
||||||
|
def claim
|
||||||
|
account_claim_url(object.account, id: object.device_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fingerprint_key
|
||||||
|
object
|
||||||
|
end
|
||||||
|
|
||||||
|
def identity_key
|
||||||
|
object
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,61 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::EncryptedMessageSerializer < ActivityPub::Serializer
|
||||||
|
context :security
|
||||||
|
|
||||||
|
context_extensions :olm
|
||||||
|
|
||||||
|
class DeviceSerializer < ActivityPub::Serializer
|
||||||
|
attributes :type, :device_id
|
||||||
|
|
||||||
|
def type
|
||||||
|
'Device'
|
||||||
|
end
|
||||||
|
|
||||||
|
def device_id
|
||||||
|
object
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class DigestSerializer < ActivityPub::Serializer
|
||||||
|
attributes :type, :digest_algorithm, :digest_value
|
||||||
|
|
||||||
|
def type
|
||||||
|
'Digest'
|
||||||
|
end
|
||||||
|
|
||||||
|
def digest_algorithm
|
||||||
|
'http://www.w3.org/2000/09/xmldsig#hmac-sha256'
|
||||||
|
end
|
||||||
|
|
||||||
|
def digest_value
|
||||||
|
object
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attributes :type, :message_type, :cipher_text, :message_franking
|
||||||
|
|
||||||
|
has_one :attributed_to, serializer: DeviceSerializer
|
||||||
|
has_one :to, serializer: DeviceSerializer
|
||||||
|
has_one :digest, serializer: DigestSerializer
|
||||||
|
|
||||||
|
def type
|
||||||
|
'EncryptedMessage'
|
||||||
|
end
|
||||||
|
|
||||||
|
def attributed_to
|
||||||
|
object.source_device.device_id
|
||||||
|
end
|
||||||
|
|
||||||
|
def to
|
||||||
|
object.target_device_id
|
||||||
|
end
|
||||||
|
|
||||||
|
def message_type
|
||||||
|
object.type
|
||||||
|
end
|
||||||
|
|
||||||
|
def cipher_text
|
||||||
|
object.body
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,35 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::OneTimeKeySerializer < ActivityPub::Serializer
|
||||||
|
context :security
|
||||||
|
|
||||||
|
context_extensions :olm
|
||||||
|
|
||||||
|
class SignatureSerializer < ActivityPub::Serializer
|
||||||
|
attributes :type, :signature_value
|
||||||
|
|
||||||
|
def type
|
||||||
|
'Ed25519Signature'
|
||||||
|
end
|
||||||
|
|
||||||
|
def signature_value
|
||||||
|
object.signature
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attributes :key_id, :type, :public_key_base64
|
||||||
|
|
||||||
|
has_one :signature, serializer: SignatureSerializer
|
||||||
|
|
||||||
|
def type
|
||||||
|
'Curve25519Key'
|
||||||
|
end
|
||||||
|
|
||||||
|
def public_key_base64
|
||||||
|
object.key
|
||||||
|
end
|
||||||
|
|
||||||
|
def signature
|
||||||
|
object
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::EncryptedMessageSerializer < ActiveModel::Serializer
|
||||||
|
attributes :id, :account_id, :device_id,
|
||||||
|
:type, :body, :digest, :message_franking,
|
||||||
|
:created_at
|
||||||
|
|
||||||
|
def id
|
||||||
|
object.id.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_id
|
||||||
|
object.from_account_id.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def device_id
|
||||||
|
object.from_device_id
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::Keys::ClaimResultSerializer < ActiveModel::Serializer
|
||||||
|
attributes :account_id, :device_id, :key_id, :key, :signature
|
||||||
|
|
||||||
|
def account_id
|
||||||
|
object.account.id.to_s
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,6 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::Keys::DeviceSerializer < ActiveModel::Serializer
|
||||||
|
attributes :device_id, :name, :identity_key,
|
||||||
|
:fingerprint_key
|
||||||
|
end
|
@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::Keys::QueryResultSerializer < ActiveModel::Serializer
|
||||||
|
attributes :account_id
|
||||||
|
|
||||||
|
has_many :devices, serializer: REST::Keys::DeviceSerializer
|
||||||
|
|
||||||
|
def account_id
|
||||||
|
object.account.id.to_s
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,70 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ClearDomainMediaService < BaseService
|
||||||
|
attr_reader :domain_block
|
||||||
|
|
||||||
|
def call(domain_block)
|
||||||
|
@domain_block = domain_block
|
||||||
|
clear_media! if domain_block.reject_media?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def invalidate_association_caches!
|
||||||
|
# Normally, associated models of a status are immutable (except for accounts)
|
||||||
|
# so they are aggressively cached. After updating the media attachments to no
|
||||||
|
# longer point to a local file, we need to clear the cache to make those
|
||||||
|
# changes appear in the API and UI
|
||||||
|
@affected_status_ids.each { |id| Rails.cache.delete_matched("statuses/#{id}-*") }
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_media!
|
||||||
|
@affected_status_ids = []
|
||||||
|
|
||||||
|
begin
|
||||||
|
clear_account_images!
|
||||||
|
clear_account_attachments!
|
||||||
|
clear_emojos!
|
||||||
|
ensure
|
||||||
|
invalidate_association_caches!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_account_images!
|
||||||
|
blocked_domain_accounts.reorder(nil).find_each do |account|
|
||||||
|
account.avatar.destroy if account.avatar&.exists?
|
||||||
|
account.header.destroy if account.header&.exists?
|
||||||
|
account.save
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_account_attachments!
|
||||||
|
media_from_blocked_domain.reorder(nil).find_each do |attachment|
|
||||||
|
@affected_status_ids << attachment.status_id if attachment.status_id.present?
|
||||||
|
|
||||||
|
attachment.file.destroy if attachment.file&.exists?
|
||||||
|
attachment.type = :unknown
|
||||||
|
attachment.save
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_emojos!
|
||||||
|
emojis_from_blocked_domains.destroy_all
|
||||||
|
end
|
||||||
|
|
||||||
|
def blocked_domain
|
||||||
|
domain_block.domain
|
||||||
|
end
|
||||||
|
|
||||||
|
def blocked_domain_accounts
|
||||||
|
Account.by_domain_and_subdomains(blocked_domain)
|
||||||
|
end
|
||||||
|
|
||||||
|
def media_from_blocked_domain
|
||||||
|
MediaAttachment.joins(:account).merge(blocked_domain_accounts).reorder(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def emojis_from_blocked_domains
|
||||||
|
CustomEmoji.by_domain_and_subdomains(blocked_domain)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,78 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class DeliverToDeviceService < BaseService
|
||||||
|
include Payloadable
|
||||||
|
|
||||||
|
class EncryptedMessage < ActiveModelSerializers::Model
|
||||||
|
attributes :source_account, :target_account, :source_device,
|
||||||
|
:target_device_id, :type, :body, :digest,
|
||||||
|
:message_franking
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(source_account, source_device, options = {})
|
||||||
|
@source_account = source_account
|
||||||
|
@source_device = source_device
|
||||||
|
@target_account = Account.find(options[:account_id])
|
||||||
|
@target_device_id = options[:device_id]
|
||||||
|
@body = options[:body]
|
||||||
|
@type = options[:type]
|
||||||
|
@hmac = options[:hmac]
|
||||||
|
|
||||||
|
set_message_franking!
|
||||||
|
|
||||||
|
if @target_account.local?
|
||||||
|
deliver_to_local!
|
||||||
|
else
|
||||||
|
deliver_to_remote!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_message_franking!
|
||||||
|
@message_franking = message_franking.to_token
|
||||||
|
end
|
||||||
|
|
||||||
|
def deliver_to_local!
|
||||||
|
target_device = @target_account.devices.find_by!(device_id: @target_device_id)
|
||||||
|
|
||||||
|
target_device.encrypted_messages.create!(
|
||||||
|
from_account: @source_account,
|
||||||
|
from_device_id: @source_device.device_id,
|
||||||
|
type: @type,
|
||||||
|
body: @body,
|
||||||
|
digest: @hmac,
|
||||||
|
message_franking: @message_franking
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def deliver_to_remote!
|
||||||
|
ActivityPub::DeliveryWorker.perform_async(
|
||||||
|
Oj.dump(serialize_payload(ActivityPub::ActivityPresenter.from_encrypted_message(encrypted_message), ActivityPub::ActivitySerializer)),
|
||||||
|
@source_account.id,
|
||||||
|
@target_account.inbox_url
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def message_franking
|
||||||
|
MessageFranking.new(
|
||||||
|
source_account_id: @source_account.id,
|
||||||
|
target_account_id: @target_account.id,
|
||||||
|
hmac: @hmac,
|
||||||
|
timestamp: Time.now.utc
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def encrypted_message
|
||||||
|
EncryptedMessage.new(
|
||||||
|
source_account: @source_account,
|
||||||
|
target_account: @target_account,
|
||||||
|
source_device: @source_device,
|
||||||
|
target_device_id: @target_device_id,
|
||||||
|
type: @type,
|
||||||
|
body: @body,
|
||||||
|
digest: @hmac,
|
||||||
|
message_franking: @message_franking
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,77 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Keys::ClaimService < BaseService
|
||||||
|
HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze
|
||||||
|
|
||||||
|
class Result < ActiveModelSerializers::Model
|
||||||
|
attributes :account, :device_id, :key_id,
|
||||||
|
:key, :signature
|
||||||
|
|
||||||
|
def initialize(account, device_id, key_attributes = {})
|
||||||
|
@account = account
|
||||||
|
@device_id = device_id
|
||||||
|
@key_id = key_attributes[:key_id]
|
||||||
|
@key = key_attributes[:key]
|
||||||
|
@signature = key_attributes[:signature]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(source_account, target_account_id, device_id)
|
||||||
|
@source_account = source_account
|
||||||
|
@target_account = Account.find(target_account_id)
|
||||||
|
@device_id = device_id
|
||||||
|
|
||||||
|
if @target_account.local?
|
||||||
|
claim_local_key!
|
||||||
|
else
|
||||||
|
claim_remote_key!
|
||||||
|
end
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def claim_local_key!
|
||||||
|
device = @target_account.devices.find_by(device_id: @device_id)
|
||||||
|
key = nil
|
||||||
|
|
||||||
|
ApplicationRecord.transaction do
|
||||||
|
key = device.one_time_keys.order(Arel.sql('random()')).first!
|
||||||
|
key.destroy!
|
||||||
|
end
|
||||||
|
|
||||||
|
@result = Result.new(@target_account, @device_id, key)
|
||||||
|
end
|
||||||
|
|
||||||
|
def claim_remote_key!
|
||||||
|
query_result = QueryService.new.call(@target_account)
|
||||||
|
device = query_result.find(@device_id)
|
||||||
|
|
||||||
|
return unless device.present? && device.valid_claim_url?
|
||||||
|
|
||||||
|
json = fetch_resource_with_post(device.claim_url)
|
||||||
|
|
||||||
|
return unless json.present? && json['publicKeyBase64'].present?
|
||||||
|
|
||||||
|
@result = Result.new(@target_account, @device_id, key_id: json['id'], key: json['publicKeyBase64'], signature: json.dig('signature', 'signatureValue'))
|
||||||
|
rescue HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error => e
|
||||||
|
Rails.logger.debug "Claiming one-time key for #{@target_account.acct}:#{@device_id} failed: #{e}"
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_resource_with_post(uri)
|
||||||
|
build_post_request(uri).perform do |response|
|
||||||
|
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response)
|
||||||
|
|
||||||
|
body_to_json(response.body_with_limit) if response.code == 200
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_post_request(uri)
|
||||||
|
Request.new(:post, uri).tap do |request|
|
||||||
|
request.on_behalf_of(@source_account, :uri)
|
||||||
|
request.add_headers(HEADERS)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,75 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Keys::QueryService < BaseService
|
||||||
|
include JsonLdHelper
|
||||||
|
|
||||||
|
class Result < ActiveModelSerializers::Model
|
||||||
|
attributes :account, :devices
|
||||||
|
|
||||||
|
def initialize(account, devices)
|
||||||
|
@account = account
|
||||||
|
@devices = devices || []
|
||||||
|
end
|
||||||
|
|
||||||
|
def find(device_id)
|
||||||
|
@devices.find { |device| device.device_id == device_id }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Device < ActiveModelSerializers::Model
|
||||||
|
attributes :device_id, :name, :identity_key, :fingerprint_key
|
||||||
|
|
||||||
|
def initialize(attributes = {})
|
||||||
|
@device_id = attributes[:device_id]
|
||||||
|
@name = attributes[:name]
|
||||||
|
@identity_key = attributes[:identity_key]
|
||||||
|
@fingerprint_key = attributes[:fingerprint_key]
|
||||||
|
@claim_url = attributes[:claim_url]
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_claim_url?
|
||||||
|
return false if @claim_url.blank?
|
||||||
|
|
||||||
|
begin
|
||||||
|
parsed_url = Addressable::URI.parse(@claim_url).normalize
|
||||||
|
rescue Addressable::URI::InvalidURIError
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
%w(http https).include?(parsed_url.scheme) && parsed_url.host.present?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(account)
|
||||||
|
@account = account
|
||||||
|
|
||||||
|
if @account.local?
|
||||||
|
query_local_devices!
|
||||||
|
else
|
||||||
|
query_remote_devices!
|
||||||
|
end
|
||||||
|
|
||||||
|
Result.new(@account, @devices)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def query_local_devices!
|
||||||
|
@devices = @account.devices.map { |device| Device.new(device) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_remote_devices!
|
||||||
|
return if @account.devices_url.blank?
|
||||||
|
|
||||||
|
json = fetch_resource(@account.devices_url)
|
||||||
|
|
||||||
|
return if json['items'].blank?
|
||||||
|
|
||||||
|
@devices = json['items'].map do |device|
|
||||||
|
Device.new(device_id: device['id'], name: device['name'], identity_key: device.dig('identityKey', 'publicKeyBase64'), fingerprint_key: device.dig('fingerprintKey', 'publicKeyBase64'), claim_url: device['claim'])
|
||||||
|
end
|
||||||
|
rescue HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error => e
|
||||||
|
Rails.logger.debug "Querying devices for #{@account.acct} failed: #{e}"
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Ed25519KeyValidator < ActiveModel::EachValidator
|
||||||
|
def validate_each(record, attribute, value)
|
||||||
|
return if value.blank?
|
||||||
|
|
||||||
|
key = Base64.decode64(value)
|
||||||
|
|
||||||
|
record.errors[attribute] << I18n.t('crypto.errors.invalid_key') unless verified?(key)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def verified?(key)
|
||||||
|
Ed25519.validate_key_bytes(key)
|
||||||
|
rescue ArgumentError
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,29 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Ed25519SignatureValidator < ActiveModel::EachValidator
|
||||||
|
def validate_each(record, attribute, value)
|
||||||
|
return if value.blank?
|
||||||
|
|
||||||
|
verify_key = Ed25519::VerifyKey.new(Base64.decode64(option_to_value(record, :verify_key)))
|
||||||
|
signature = Base64.decode64(value)
|
||||||
|
message = option_to_value(record, :message)
|
||||||
|
|
||||||
|
record.errors[attribute] << I18n.t('crypto.errors.invalid_signature') unless verified?(verify_key, signature, message)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def verified?(verify_key, signature, message)
|
||||||
|
verify_key.verify(signature, message)
|
||||||
|
rescue Ed25519::VerifyError, ArgumentError
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def option_to_value(record, key)
|
||||||
|
if options[key].is_a?(Proc)
|
||||||
|
options[key].call(record)
|
||||||
|
else
|
||||||
|
record.public_send(options[key])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,14 @@
|
|||||||
|
- content_for :page_title do
|
||||||
|
= t('auth.login')
|
||||||
|
|
||||||
|
= simple_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
|
||||||
|
%p.hint.otp-hint= t('users.suspicious_sign_in_confirmation')
|
||||||
|
|
||||||
|
.fields-group
|
||||||
|
= f.input :sign_in_token_attempt, type: :number, wrapper: :with_label, label: t('simple_form.labels.defaults.sign_in_token_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.sign_in_token_attempt'), :autocomplete => 'off' }, autofocus: true
|
||||||
|
|
||||||
|
.actions
|
||||||
|
= f.button :button, t('auth.login'), type: :submit
|
||||||
|
|
||||||
|
- if Setting.site_contact_email.present?
|
||||||
|
%p.hint.subtle-hint= t('users.generic_access_help_html', email: mail_to(Setting.site_contact_email, nil))
|
@ -0,0 +1,105 @@
|
|||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell.hero
|
||||||
|
.email-row
|
||||||
|
.col-6
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.text-center.padded
|
||||||
|
%table.hero-icon.alert-icon{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td
|
||||||
|
= image_tag full_pack_url('media/images/mailer/icon_email.png'), alt: ''
|
||||||
|
|
||||||
|
%h1= t 'user_mailer.sign_in_token.title'
|
||||||
|
%p.lead= t 'user_mailer.sign_in_token.explanation'
|
||||||
|
|
||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell.content-start
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.input-cell
|
||||||
|
%table.input{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td= @resource.sign_in_token
|
||||||
|
|
||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell
|
||||||
|
.email-row
|
||||||
|
.col-6
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.text-center
|
||||||
|
%p= t 'user_mailer.sign_in_token.details'
|
||||||
|
%tr
|
||||||
|
%td.column-cell.text-center
|
||||||
|
%p
|
||||||
|
%strong= "#{t('sessions.ip')}:"
|
||||||
|
= @remote_ip
|
||||||
|
%br/
|
||||||
|
%strong= "#{t('sessions.browser')}:"
|
||||||
|
%span{ title: @user_agent }= t 'sessions.description', browser: t("sessions.browsers.#{@detection.id}", default: "#{@detection.id}"), platform: t("sessions.platforms.#{@detection.platform.id}", default: "#{@detection.platform.id}")
|
||||||
|
%br/
|
||||||
|
= l(@timestamp)
|
||||||
|
|
||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell
|
||||||
|
.email-row
|
||||||
|
.col-6
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.text-center
|
||||||
|
%p= t 'user_mailer.sign_in_token.further_actions'
|
||||||
|
|
||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.button-cell
|
||||||
|
%table.button{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.button-primary
|
||||||
|
= link_to edit_user_registration_url do
|
||||||
|
%span= t 'settings.account_settings'
|
@ -0,0 +1,17 @@
|
|||||||
|
<%= t 'user_mailer.sign_in_token.title' %>
|
||||||
|
|
||||||
|
===
|
||||||
|
|
||||||
|
<%= t 'user_mailer.sign_in_token.explanation' %>
|
||||||
|
|
||||||
|
=> <%= @resource.sign_in_token %>
|
||||||
|
|
||||||
|
<%= t 'user_mailer.sign_in_token.details' %>
|
||||||
|
|
||||||
|
<%= t('sessions.ip') %>: <%= @remote_ip %>
|
||||||
|
<%= t('sessions.browser') %>: <%= t('sessions.description', browser: t("sessions.browsers.#{@detection.id}", default: "#{@detection.id}"), platform: t("sessions.platforms.#{@detection.platform.id}", default: "#{@detection.platform.id}")) %>
|
||||||
|
<%= l(@timestamp) %>
|
||||||
|
|
||||||
|
<%= t 'user_mailer.sign_in_token.further_actions' %>
|
||||||
|
|
||||||
|
=> <%= edit_user_registration_url %>
|
@ -0,0 +1,14 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class DomainClearMediaWorker
|
||||||
|
include Sidekiq::Worker
|
||||||
|
|
||||||
|
sidekiq_options queue: 'pull'
|
||||||
|
|
||||||
|
def perform(domain_block_id)
|
||||||
|
domain_block = DomainBlock.find_by(id: domain_block_id)
|
||||||
|
return true if domain_block.nil?
|
||||||
|
|
||||||
|
ClearDomainMediaService.new.call(domain_block)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class PushEncryptedMessageWorker
|
||||||
|
include Sidekiq::Worker
|
||||||
|
include Redisable
|
||||||
|
|
||||||
|
def perform(encrypted_message_id)
|
||||||
|
encrypted_message = EncryptedMessage.find(encrypted_message_id)
|
||||||
|
message = InlineRenderer.render(encrypted_message, nil, :encrypted_message)
|
||||||
|
timeline_id = "timeline:#{encrypted_message.device.account_id}:#{encrypted_message.device.device_id}"
|
||||||
|
|
||||||
|
redis.publish(timeline_id, Oj.dump(event: :encrypted_message, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i))
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue