Initial commit

This commit is contained in:
Yaser
2026-08-13 19:56:46 +03:30
commit de1d57a67b
474 changed files with 43185 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
# frozen_string_literal: true
class APIKeysVerifier
def initialize(params = {})
@kid = params[:kid]
@signature = params[:signature]
@nonce = params[:nonce] || nil
@api_key = APIKey.find_by!(kid: @kid)
end
def verify_hmac_payload?
data = @nonce.to_s + @kid
algorithm = 'SHA' + @api_key.algorithm[2..4]
true_signature = OpenSSL::HMAC.hexdigest(algorithm, @api_key.secret, data)
true_signature == @signature
end
end

View File

@@ -0,0 +1,66 @@
# frozen_string_literal: true
require 'digest'
require 'net/http'
module CaptchaService
# Google recaptcha verifier
class RecaptchaVerifier
include Recaptcha::Adapters::ControllerMethods
attr_reader :request
def initialize(request:)
@request = request
end
def response_valid?(skip_remote_ip:, response:)
# according to changes in v2 support https://github.com/ambethia/recaptcha#recaptcha-v2-api-and-usage
# method comes from Recaptcha::Adapters::ControllerMethods
# https://github.com/ambethia/recaptcha/blob/master/lib/recaptcha/adapters/controller_methods.rb#L10
verify_recaptcha(secret_key: Barong::App.config.recaptcha_secret_key,
model: User,
skip_remote_ip: skip_remote_ip,
response: response)
end
end
# Geetest.com captcha verifier
class GeetestVerifier
def initialize
@api = 'http://api.geetest.com'
@validate_path = '/validate.php'
@register_path = '/register.php'
@geetest_id = Barong::App.config.geetest_id
@geetest_key = Barong::App.config.geetest_key
end
def validate(response)
md5 = Digest::MD5.hexdigest(@geetest_key + 'geetest' + response['geetest_challenge'])
if response['geetest_validate'] == md5
back = begin
post(@api + @validate_path, seccode: response['geetest_seccode'])
rescue StandardError
''
end
return back == Digest::MD5.hexdigest(response['geetest_seccode'])
end
false
end
def register
challenge = get(@api + @register_path + "?gt=#{@geetest_id}")
{ gt: @geetest_id, challenge: Digest::MD5.hexdigest(challenge + @geetest_key) }
rescue StandardError
''
end
def get(uri)
Net::HTTP.get_response(URI(uri)).body
end
def post(uri, data)
Net::HTTP.post_form(URI(uri), data).body
end
end
end

View File

@@ -0,0 +1,75 @@
# frozen_string_literal: true
class EncryptionService
# Example: 202040
# Week number starts from 0
def self.current_salt
Time.now.strftime('%Y%W')
end
def self.pack(salt, value)
[salt, value].join('.')
end
def self.unpack(str)
raise "Invalid encrypted value: #{str}" unless str =~ (/(\w*)\.(.*)/)
[$1, $2]
end
def self.encrypt(value)
# Get current salt
salt = current_salt
# Get or generate new master key from salt
current_key = get_master_key(salt)
# Encrypt attribute value
encrypted_key = encryptor(current_key).encrypt_and_sign(value)
# Add salt before encrypted value
pack(salt, encrypted_key)
end
def self.decrypt(value)
# Unpack salt and encrypted_key
salt, encrypted_key = unpack(value)
# Get master key from salt
master_key = get_master_key(salt)
# Decrypt encrypted value for attribute
encryptor(master_key).decrypt_and_verify(encrypted_key)
end
private
def self.encryptor(key)
ActiveSupport::MessageEncryptor.new(key)
end
def self.get_master_key(salt)
@cache ||= {}
# Delete keys with expired date
delete_expired_keys
unless @cache[salt]
# Initialize hash for specific salt
@cache[salt] = {}
# Put key value from key generator
@cache[salt]['key'] = ActiveSupport::KeyGenerator.new(
ENV.fetch('SECRET_KEY_BASE')
).generate_key(salt, ActiveSupport::MessageEncryptor.key_len)
# Put expire date for specific key
@cache[salt]['expire_date'] = 1.week.from_now
end
@cache[salt]['key']
end
def self.delete_expired_keys
return unless @cache.is_a?(Hash)
# Iterate through all @cache values
@cache.each do |salt, values|
# Delete key if expire date expired
@cache.delete(salt) if values['expire_date'].present? && values['expire_date'] < Time.now
end
end
end

View File

@@ -0,0 +1,219 @@
# frozen_string_literal: true
require 'bunny'
require 'ostruct'
class EventMailer
Error = Class.new(StandardError)
class VerificationError < Error; end
def initialize(events, exchanges, keychain)
@exchanges = exchanges
@keychain = keychain
@events = events
Kernel.at_exit { unlisten }
end
def call
listen
end
private
def listen
unlisten
@bunny_session = Bunny::Session.new(rabbitmq_credentials).tap do |session|
session.start
Kernel.at_exit { session.stop }
end
@bunny_channel = @bunny_session.channel
# Delete old queue if some exists
@bunny_channel.queue_delete('barong.postmaster.event.mailer') if @bunny_session.queue_exists?('barong.postmaster.event.mailer')
# Define fanout exchanges which will broadcast
# all the messages they receives to all the queues they know
retry_exchange = @bunny_channel.fanout('barong.event.mailer.retry.exchange')
main_exchange = @bunny_channel.fanout('barong.event.mailer.main.exchange')
queue = @bunny_channel.queue('barong.event.mailer.main', auto_delete: false, durable: true,
arguments: {
:'x-dead-letter-exchange' => retry_exchange.name,
}
)
queue.bind(main_exchange)
retry_queue = @bunny_channel.queue('barong.event.mailer.retry', auto_delete: false, durable: true,
arguments: {
:'x-dead-letter-exchange' => main_exchange.name,
:'x-message-ttl' => 120000 # will trigger retry every 2 minutes
})
retry_queue.bind(retry_exchange)
@events.each do |event|
exchange_name = @exchanges[event[:exchange].to_sym][:name]
exchange = @bunny_channel.direct(exchange_name)
queue.bind(exchange, routing_key: event[:key])
end
Rails.logger.info { 'Listening for events.' }
queue.subscribe(manual_ack: true, block: true, &method(:handle_message))
end
def unlisten
if @bunny_session || @bunny_channel
Rails.logger.info { 'No longer listening for events.' }
end
@bunny_channel&.work_pool&.kill
@bunny_session&.stop
ensure
@bunny_channel = nil
@bunny_session = nil
end
def algorithm_verification_options(signer)
{ algorithms: @keychain[signer][:algorithm] }
end
def jwt_public_key(signer)
OpenSSL::PKey.read(Base64.urlsafe_decode64(@keychain[signer][:value]))
end
def rabbitmq_credentials
if Barong::App.config.event_api_rabbitmq_url.present?
Barong::App.config.event_api_rabbitmq_url
else
{
host: Barong::App.config.event_api_rabbitmq_host,
port: Barong::App.config.event_api_rabbitmq_port,
username: Barong::App.config.event_api_rabbitmq_username,
password: Barong::App.config.event_api_rabbitmq_password
}
end
end
def handle_message(delivery_info, _metadata, payload)
Rails.logger.info { "Start handling a message" }
Rails.logger.info { "\nPayload: \n #{payload} \n\n Metadata: \n #{_metadata} \n\n Delivery info: \n #{delivery_info} \n" }
exchange = @exchanges.select { |_, ex| ex[:name] == delivery_info[:exchange] }
# In case of retry message
# we should get exchange name from _metadata info
if exchange.empty?
exchange_name = _metadata[:headers]['x-death'][1]['exchange']
exchange = @exchanges.select { |_, ex| ex[:name] == exchange_name }
end
exchange_id = exchange.keys.first.to_s
signer = exchange[exchange_id.to_sym][:signer]
result = verify_jwt(payload, signer.to_sym)
raise VerificationError, "Failed to verify signature from #{signer}." \
unless result[:verified].include?(signer.to_sym)
config = @events.select do |event|
event[:key] == delivery_info[:routing_key] &&
event[:exchange] == exchange_id
end.first
event = result[:payload].fetch(:event)
obj = JSON.parse(event.to_json, object_class: OpenStruct)
user = User.includes(:profiles).find_by(uid: obj.record.user.uid)
language = user.language.downcase.to_sym
Rails.logger.info { "User #{user.email} has '#{language}' email language" }
template_config = config[:templates].transform_keys(&:downcase)
unless template_config.keys.include?(language)
Rails.logger.error { "Language #{language} is not supported. Skipping." }
return
end
if config[:expression].present? && skip_event(event, config[:expression])
Rails.logger.info { "Event #{obj.name} skipped" }
return
end
params = {
logo: Barong::App.config.smtp_logo_link,
subject: template_config[language][:subject],
template_name: template_config[language][:template_path],
record: obj.record,
changes: obj.changes,
user: user
}
Postmaster.process_payload(params).deliver_now
# Acknowledges a message
# Acknowledged message is completely removed from the queue
@bunny_channel.ack(delivery_info.delivery_tag)
rescue StandardError => e
Rails.logger.error { e.inspect }
if e.is_a?(JWT::ExpiredSignature) || e.is_a?(JWT::VerificationError) || e.is_a?(VerificationError)
# Acknowledges a message
@bunny_channel.ack(delivery_info.delivery_tag)
else
# Rejects a message
# A rejected message dropped by RabbitMQ and goes to dead letter exchange queue
@bunny_channel.reject(delivery_info.delivery_tag)
end
unlisten if db_connection_error?(e)
end
def verify_jwt(payload, signer)
options = algorithm_verification_options(signer)
JWT::Multisig.verify_jwt JSON.parse(payload), { signer => jwt_public_key(signer) },
options.compact
end
def skip_event(event, expression)
# valid operators: and / or / not
operator = expression.keys.first.downcase
# { field_name: field_value }
values = expression[operator]
# return array of boolean [false, true]
res = values.keys.map do |field_name|
safe_dig(event, field_name.to_s.split('.')) == values[field_name]
end
# all? works as AND operator, any? works as OR operator
return false if (operator == :and && res.all?) || (operator == :or && res.any?) ||
(operator == :not && !res.all?)
return true if operator == :not && res.all?
true
end
def db_connection_error?(exception)
exception.is_a?(Mysql2::Error::ConnectionError) || exception.cause.is_a?(Mysql2::Error)
end
def safe_dig(hash, keypath, default = nil)
stringified_hash = JSON.parse(hash.to_json)
stringified_keypath = keypath.map(&:to_s)
stringified_keypath.reduce(stringified_hash) do |accessible, key|
return default unless accessible.is_a? Hash
return default unless accessible.key? key
accessible[key]
end
end
class << self
def call(*args)
new(*args).call
end
end
end

View File

@@ -0,0 +1,110 @@
class JibitService
attr_reader :base_url
attr_reader :api_key
attr_reader :api_secret
attr_reader :conn
attr_accessor :response
attr_accessor :access_token
attr_accessor :refresh_token
attr_accessor :called_method
# one day
JIBIT_EXPIRE_TIME = 86400
def initialize
@base_url = Barong::App.config.jibit_api_url
@api_key = Barong::App.config.jibit_api_key
@api_secret = Barong::App.config.jibit_api_secret
@conn = Faraday.new(base_url)
conn.headers['Content-Type'] = 'application/json'
@access_token = read_access_token
end
def retry_options
{ max: 5,
retry_statuses: [401, 409, 500] }
end
def read_access_token
return Rails.cache.read('jibit_access_token') if Rails.cache.read('jibit_access_token').present?
generate_access_token
raise 'please try again(jibit not create token)' unless access_token.present?
Rails.cache.write('jibit_access_token', access_token, expires_in: JIBIT_EXPIRE_TIME)
access_token
end
def generate_access_token
@called_method = __method__.to_s
@response = conn.post('tokens/generate') do |req|
req.body = { apiKey: api_key, secretKey: api_secret }.to_json
end
@access_token, @refresh_token = parse('accessToken', 'refreshToken')
end
def generate_refresh_token
@called_method = __method__.to_s
@response = conn.post('tokens/refresh') do |req|
req.body = { accessToken: access_token, refreshToken: refresh_token }.to_json
end
@access_token, @refresh_token = parse('accessToken', 'refreshToken')
end
def old_iban_info(iban)
@called_method = __method__.to_s
conn.authorization :Bearer, access_token
@response = conn.post('services/ibanInfo') do |req|
req.body = { iban: iban }.to_json
end
end
def iban_info(iban)
@called_method = __method__.to_s
conn.authorization :Bearer, access_token
@response = conn.get('ibans') do |req|
req.params[:value] = iban
end
end
def old_card_info(card_number)
@called_method = __method__.to_s
conn.authorization :Bearer, access_token
@response = conn.post('services/cardInfo') do |req|
req.body = { cardNumber: card_number }.to_json
end
end
def card_info(card_number)
@called_method = __method__.to_s
conn.authorization :Bearer, access_token
@response = conn.get('cards') do |req|
req.params[:number] = card_number
end
end
def old_mobile_info(number, national_code)
@called_method = __method__.to_s
conn.authorization :Bearer, access_token
@response = conn.post('services/matchNationalCodeAndMobileNumber') do |req|
req.body = { nationalCode: national_code, mobile: number }.to_json
end
end
def mobile_info(number, national_code)
@called_method = __method__.to_s
conn.authorization :Bearer, access_token
@response = conn.get('services/matching') do |req|
req.params[:nationalCode] = national_code
req.params[:mobileNumber] = number
end
end
private
def parse(*keys)
parsed_data = JSON.parse(response.body)
keys.map { |key| parsed_data.dig(key) }
end
end

View File

@@ -0,0 +1,43 @@
# frozen_string_literal: true
# kavenegar sms sender
class KaveNegarSmsService
class << self
def send_confirmation(user)
Rails.logger.info("Sending SMS to #{user.phones.last.number}")
totp = TOTPServiceAction.new('register-mobile')
totp.create(user.uid, user.email, period: '120')
user.write_cache('register_mobile', 'true', 120)
send_sms(user.mobile('pending').number.to_s, totp.read_code(user.uid))
end
def send_call_confirmation(user)
Rails.logger.info("Sending call to #{user.phones.find_by(category: 'landline').number}")
totp = TOTPServiceAction.new('register-phone')
totp.create(user.uid, user.email, period: '120')
user.write_cache('register_phone', 'true', 120)
send_call(user.phones.find_by(category: 'landline').number.to_s, totp.read_code(user.uid))
end
def send_call(number, code)
client = KaveRestApi::Lookup.new({ receptor: number,
token: code,
template: 'phone',
type: 'call' # or call
})
client.call.valid? ? true : false
end
def send_sms(number, code)
client = KaveRestApi::Lookup.new({ receptor: number,
token: code,
template: 'mobile',
type: 'sms' # or call
})
client.call.valid? ? true : false
end
end
end

View File

@@ -0,0 +1,65 @@
# frozen_string_literal: true
class KycService
# passport front + selfie OR driver license front and back + selfie IR id card front and back + selfie
REQUIRED_DOC_AMOUNT = { 'Passport': 2, 'Driver license': 3, 'Identity card': 3 }.freeze
class << self
def profile_step(profile)
user = profile.user
profile_label = user.labels.find_by(key: :profile)
if profile_label.nil? # first profile everw
user.labels.create(key: :profile, value: profile.state, scope: :private)
else
profile_label.update(value: profile.state) # re-submitted profile
end
return unless profile.state == 'verified'
# verify treasury after profile verified
KYC.const_get(Barong::App.config.kyc_provider.capitalize, false)::TreasuryWorker.perform_async(profile.id)
end
def document_step(document)
user = document.user
user_document_label = user.labels.find_by(key: document.doc_type.downcase)
if user_document_label.nil? # first document ever
user.labels.create(key: document.doc_type.downcase, value: document.state, scope: :private)
else
return if user_document_label.value == 'verified'
user_document_label.update(value: document.state) # re-submitted document
end
end
def kycaid_callback(params)
422
end
def treasury_label_update(treasury)
user = treasury&.user
return unless user.present?
treasury_label = user.labels.find_by(key: treasury.kind)
if treasury_label.nil? # first profile ever
user.labels.create(key: treasury.kind, value: treasury.state, scope: :private)
else
treasury.ownership! if treasury.state == 'processing' && treasury.result.present?
# user must have just one kind of treasury and new treasury like card, must not change before verified card
treasury_label.update(value: treasury.reload.state) unless treasury_label.value == 'verified'
end
end
def phone_label_update(phone)
user = phone&.user
return unless user.present?
phone_label = user.labels.find_by(key: phone.step)
if phone_label.nil?
user.labels.create(key: phone.step, value: phone.state, scope: :private)
else
phone_label.update(value: phone.state)
end
end
end
end

View File

@@ -0,0 +1,24 @@
# frozen_string_literal: true
# twilio sms sender
class MockPhoneVerifyService
class << self
def send_confirmation(phone, _channel)
Rails.logger.info("Sending SMS to #{phone.number}")
send_sms(number: phone.number,
content: Barong::App.config.sms_content_template.gsub(/{{code}}/, phone.code))
end
def send_sms(number:, content:)
from_phone = Barong::App.config.twilio_phone_number
client = Barong::MockSMS.new('', '')
client.messages.create(from: from_phone, to: '+' + number, body: content)
end
# always return true
def verify_code?(number:, code:, user:)
user.phones.find_by_number(number).present?
end
end
end

View File

@@ -0,0 +1,23 @@
# frozen_string_literal: true
# password entropy calculation
class PasswordStrengthChecker
class <<self
# this method typically called by validate! from model and from API /pass/validate controller
def calculate_entropy(password)
@checker ||= StrongPassword::StrengthChecker.new(min_entropy: Barong::App.config.password_min_entropy,
use_dictionary: Barong::App.config.password_use_dictionary)
@checker.calculate_entropy(password)
end
# User model invokes this method while validating password on create and update
def validate!(password)
password_regex = Barong::App.config.password_regexp
return 'requirements.short' unless password_regex.match(password)
return 'weak' if calculate_entropy(password) < Barong::App.config.password_min_entropy
'strong'
end
end
end

View File

@@ -0,0 +1,57 @@
# frozen_string_literal: true
module SecretStorage
Error = Class.new(StandardError)
class <<self
def server_available?
read_data('sys/health').present?
rescue StandardError
false
end
def store_secret(secret, kid)
write!(secret_path(kid), secret) unless exist?(kid)
end
def get_secret(kid)
read(secret_path(kid)) if exist?(kid)
end
def exist?(kid)
read(secret_path(kid)).present?
end
private
def secret_path(kid)
"secret/barong/api_key/#{kid}"
end
def with_human_error
yield
rescue Vault::VaultError => error
Rails.logger.error { error }
raise Error, error.message
end
def read(key)
with_human_error do
Vault.logical.read(key)
end
end
def write!(key, params)
with_human_error do
Vault.logical.write(key, value: params)
end
end
def delete!(key)
with_human_error do
Vault.logical.delete(key)
end
end
end
end

View File

@@ -0,0 +1,96 @@
# frozen_string_literal: true
class TOTPService
Error = Class.new(StandardError)
class <<self
ISSUER_NAME = 'Barong'
def server_available?
read_data('sys/health').present?
rescue StandardError
false
end
def otp_secret(otp)
CGI.parse(URI.parse(otp.data[:url]).query)['secret'][0]
end
def safe_create(uid, email)
return if exist?(uid)
create(uid, email)
end
def create(uid, email)
write_data(totp_key(uid),
generate: true,
issuer: ::Barong::App.config.app_name,
account_name: email,
qr_size: 300)
end
def exist?(uid)
read_data(totp_key(uid)).present?
end
def validate?(uid, code)
return false unless exist?(uid)
write_data(totp_code_key(uid), code: code).data[:valid]
end
def delete(uid)
delete_data(totp_key(uid))
end
def with_human_error
raise ArgumentError, 'Block is required' unless block_given?
yield
rescue Vault::VaultError => e
Rails.logger.error { e }
raise Error, '2FA server is under maintenance' if e.message.include?('connection refused')
raise Error, 'This code was already used. Wait until the next time period' if e.message.include?('code already used')
raise e
end
def totp_key(uid)
"totp/keys/#{Vault.application}_#{uid}"
end
def totp_code_key(uid)
"totp/code/#{Vault.application}_#{uid}"
end
def read_data(key)
with_human_error do
vault.read(key)
end
end
def read_code(uid)
read_data(totp_code_key(uid)).data[:code]
end
def write_data(key, params)
with_human_error do
vault.write(key, params)
end
end
def delete_data(key)
with_human_error do
vault.delete(key)
end
end
def vault
Vault.logical
end
end
end

View File

@@ -0,0 +1,104 @@
# frozen_string_literal: true
class TOTPServiceAction
Error = Class.new(StandardError)
ISSUER_NAME = 'Barong'
attr_reader :action
def initialize(action)
raise 'Please determine action for totp code' if action.blank?
@action = action
end
def server_available?
read_data('sys/health').present?
rescue StandardError
false
end
def otp_secret(otp)
CGI.parse(URI.parse(otp.data[:url]).query)['secret'][0]
end
def safe_create(uid, email)
return if exist?(uid)
create(uid, email)
end
def create(uid, email, period: '120')
write_data(totp_key(uid),
generate: true,
issuer: ::Barong::App.config.app_name,
period: period, account_name: email, qr_size: 300)
end
def exist?(uid)
read_data(totp_key(uid)).present?
end
def validate?(uid, code)
return false unless exist?(uid)
result = write_data(totp_code_key(uid), code: code)
return false unless result
result.data[:valid]
end
def safe_validate?(uid, code)
return false unless exist?(uid)
code == read_code(uid)
end
def delete(uid)
delete_data(totp_key(uid))
end
def with_human_error
raise ArgumentError, 'Block is required' unless block_given?
yield
rescue Vault::VaultError => e
Rails.logger.error { e }
false
end
def totp_key(uid)
"totp/keys/#{Vault.application}_#{@action}_#{uid}"
end
def totp_code_key(uid)
"totp/code/#{Vault.application}_#{@action}_#{uid}"
end
def read_data(key)
with_human_error do
vault.read(key)
end
end
def read_code(uid)
read_data(totp_code_key(uid)).data[:code]
end
def write_data(key, params)
with_human_error { vault.write(key, params) }
end
def delete_data(key)
with_human_error do
vault.delete(key)
end
end
def vault
Vault.logical
end
end

View File

@@ -0,0 +1,28 @@
# frozen_string_literal: true
# twilio sms sender
class TwilioSmsSendService
class << self
def send_confirmation(phone, _channel)
Rails.logger.info("Sending SMS to #{phone.number}")
send_sms(number: phone.number,
content: Barong::App.config.sms_content_template.gsub(/{{code}}/, phone.code))
end
def send_sms(number:, content:)
from_phone = Barong::App.config.twilio_phone_number
client = Barong::App.config.twilio_client
client.messages.create(
from: from_phone,
to: '+' + number,
body: content
)
end
# returns true if given code matches number in DB
def verify_code?(number:, code:, user:)
user.phones.find_by_number(number, code: code)
end
end
end

View File

@@ -0,0 +1,35 @@
# frozen_string_literal: true
# twilio process verification
class TwilioVerifyService
class << self
def send_confirmation(phone, channel)
Rails.logger.info("Sending code to #{phone.number} via #{channel}")
send_code(number: phone.number, channel: channel)
end
def send_code(number:, channel:)
verify_client.services(@service_sid)
.verifications
.create(to: '+' + number, channel: channel)
end
# return true if twilio accepts given code for the given number
def verify_code?(number:, code:, user:)
status = verify_client.services(@service_sid)
.verification_checks
.create(to: '+' + number, code: code)
.status
status == 'approved'
end
def verify_client
client = Barong::App.config.twilio_client
@service_sid = Barong::App.config.twilio_service_sid
client.verify
end
end
end

View File

@@ -0,0 +1,10 @@
# frozen_string_literal: true
class UIDGenerator
def self.generate(prefix = 'ID')
loop do
uid = "%s%s" % [prefix.upcase, SecureRandom.hex(5).upcase]
return uid if User.where(uid: uid).empty? && ServiceAccount.where(uid: uid).empty?
end
end
end