castle-rb 5.0.0 → 6.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +107 -33
- data/lib/castle.rb +46 -22
- data/lib/castle/api.rb +22 -13
- data/lib/castle/api/approve_device.rb +25 -0
- data/lib/castle/api/authenticate.rb +34 -0
- data/lib/castle/api/end_impersonation.rb +29 -0
- data/lib/castle/api/get_device.rb +25 -0
- data/lib/castle/api/get_devices_for_user.rb +25 -0
- data/lib/castle/api/identify.rb +26 -0
- data/lib/castle/api/report_device.rb +25 -0
- data/lib/castle/api/review.rb +24 -0
- data/lib/castle/api/start_impersonation.rb +29 -0
- data/lib/castle/api/track.rb +26 -0
- data/lib/castle/client.rb +48 -62
- data/lib/castle/{extractors/client_id.rb → client_id/extract.rb} +2 -2
- data/lib/castle/commands/approve_device.rb +21 -0
- data/lib/castle/commands/authenticate.rb +13 -13
- data/lib/castle/commands/end_impersonation.rb +25 -0
- data/lib/castle/commands/get_device.rb +21 -0
- data/lib/castle/commands/get_devices_for_user.rb +21 -0
- data/lib/castle/commands/identify.rb +12 -13
- data/lib/castle/commands/report_device.rb +21 -0
- data/lib/castle/commands/review.rb +6 -3
- data/lib/castle/commands/start_impersonation.rb +25 -0
- data/lib/castle/commands/track.rb +12 -13
- data/lib/castle/configuration.rb +17 -19
- data/lib/castle/context/{default.rb → get_default.rb} +5 -6
- data/lib/castle/context/{merger.rb → merge.rb} +3 -3
- data/lib/castle/context/prepare.rb +18 -0
- data/lib/castle/context/{sanitizer.rb → sanitize.rb} +1 -1
- data/lib/castle/core/get_connection.rb +25 -0
- data/lib/castle/{api/response.rb → core/process_response.rb} +4 -2
- data/lib/castle/core/process_webhook.rb +20 -0
- data/lib/castle/core/send_request.rb +50 -0
- data/lib/castle/errors.rb +2 -0
- data/lib/castle/events.rb +1 -1
- data/lib/castle/failover/prepare_response.rb +23 -0
- data/lib/castle/failover/strategy.rb +20 -0
- data/lib/castle/{extractors/headers.rb → headers/extract.rb} +8 -6
- data/lib/castle/headers/filter.rb +37 -0
- data/lib/castle/headers/format.rb +24 -0
- data/lib/castle/{extractors/ip.rb → ip/extract.rb} +8 -7
- data/lib/castle/logger.rb +19 -0
- data/lib/castle/payload/prepare.rb +27 -0
- data/lib/castle/secure_mode.rb +6 -2
- data/lib/castle/session.rb +18 -0
- data/lib/castle/singleton_configuration.rb +9 -0
- data/lib/castle/utils/clean_invalid_chars.rb +24 -0
- data/lib/castle/utils/clone.rb +15 -0
- data/lib/castle/utils/deep_symbolize_keys.rb +45 -0
- data/lib/castle/utils/get_timestamp.rb +15 -0
- data/lib/castle/utils/{merger.rb → merge.rb} +3 -3
- data/lib/castle/utils/secure_compare.rb +22 -0
- data/lib/castle/validators/not_supported.rb +1 -0
- data/lib/castle/validators/present.rb +1 -0
- data/lib/castle/verdict.rb +13 -0
- data/lib/castle/version.rb +1 -1
- data/lib/castle/webhooks/verify.rb +43 -0
- data/spec/integration/rails/rails_spec.rb +33 -7
- data/spec/integration/rails/support/application.rb +3 -1
- data/spec/integration/rails/support/home_controller.rb +47 -5
- data/spec/lib/castle/api/approve_device_spec.rb +21 -0
- data/spec/lib/castle/api/authenticate_spec.rb +140 -0
- data/spec/lib/castle/api/end_impersonation_spec.rb +59 -0
- data/spec/lib/castle/api/get_device_spec.rb +19 -0
- data/spec/lib/castle/api/get_devices_for_user_spec.rb +19 -0
- data/spec/lib/castle/api/identify_spec.rb +68 -0
- data/spec/lib/castle/api/report_device_spec.rb +21 -0
- data/spec/lib/castle/{review_spec.rb → api/review_spec.rb} +3 -3
- data/spec/lib/castle/api/start_impersonation_spec.rb +59 -0
- data/spec/lib/castle/api/track_spec.rb +68 -0
- data/spec/lib/castle/api_spec.rb +16 -1
- data/spec/lib/castle/{extractors/client_id_spec.rb → client_id/extract_spec.rb} +2 -2
- data/spec/lib/castle/client_spec.rb +39 -21
- data/spec/lib/castle/commands/approve_device_spec.rb +24 -0
- data/spec/lib/castle/commands/authenticate_spec.rb +7 -16
- data/spec/lib/castle/commands/end_impersonation_spec.rb +82 -0
- data/spec/lib/castle/commands/get_device_spec.rb +24 -0
- data/spec/lib/castle/commands/get_devices_for_user_spec.rb +24 -0
- data/spec/lib/castle/commands/identify_spec.rb +5 -16
- data/spec/lib/castle/commands/report_device_spec.rb +24 -0
- data/spec/lib/castle/commands/review_spec.rb +1 -1
- data/spec/lib/castle/commands/{impersonate_spec.rb → start_impersonation_spec.rb} +7 -32
- data/spec/lib/castle/commands/track_spec.rb +5 -16
- data/spec/lib/castle/configuration_spec.rb +9 -138
- data/spec/lib/castle/context/{default_spec.rb → get_default_spec.rb} +1 -2
- data/spec/lib/castle/context/{merger_spec.rb → merge_spec.rb} +1 -1
- data/spec/lib/castle/context/prepare_spec.rb +44 -0
- data/spec/lib/castle/context/{sanitizer_spec.rb → sanitize_spec.rb} +1 -1
- data/spec/lib/castle/{api/connection_spec.rb → core/get_connection_spec.rb} +3 -3
- data/spec/lib/castle/{api/response_spec.rb → core/process_response_spec.rb} +56 -1
- data/spec/lib/castle/core/process_webhook_spec.rb +46 -0
- data/spec/lib/castle/{api/request_spec.rb → core/send_request_spec.rb} +20 -16
- data/spec/lib/castle/failover/strategy_spec.rb +12 -0
- data/spec/lib/castle/{extractors/headers_spec.rb → headers/extract_spec.rb} +7 -7
- data/spec/lib/castle/{headers_filter_spec.rb → headers/filter_spec.rb} +3 -3
- data/spec/lib/castle/headers/format_spec.rb +25 -0
- data/spec/lib/castle/{extractors/ip_spec.rb → ip/extract_spec.rb} +1 -1
- data/spec/lib/castle/logger_spec.rb +42 -0
- data/spec/lib/castle/payload/prepare_spec.rb +54 -0
- data/spec/lib/castle/{api/session_spec.rb → session_spec.rb} +6 -4
- data/spec/lib/castle/singleton_configuration_spec.rb +18 -0
- data/spec/lib/castle/utils/clean_invalid_chars_spec.rb +69 -0
- data/spec/lib/castle/utils/{cloner_spec.rb → clone_spec.rb} +3 -3
- data/spec/lib/castle/utils/deep_symbolize_keys_spec.rb +50 -0
- data/spec/lib/castle/utils/{timestamp_spec.rb → get_timestamp_spec.rb} +1 -1
- data/spec/lib/castle/utils/{merger_spec.rb → merge_spec.rb} +3 -3
- data/spec/lib/castle/verdict_spec.rb +9 -0
- data/spec/lib/castle/webhooks/verify_spec.rb +69 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/support/shared_examples/configuration.rb +129 -0
- metadata +129 -57
- data/lib/castle/api/connection.rb +0 -24
- data/lib/castle/api/request.rb +0 -42
- data/lib/castle/api/session.rb +0 -20
- data/lib/castle/commands/impersonate.rb +0 -26
- data/lib/castle/failover_auth_response.rb +0 -21
- data/lib/castle/headers_filter.rb +0 -35
- data/lib/castle/headers_formatter.rb +0 -22
- data/lib/castle/review.rb +0 -11
- data/lib/castle/utils.rb +0 -55
- data/lib/castle/utils/cloner.rb +0 -11
- data/lib/castle/utils/timestamp.rb +0 -12
- data/spec/lib/castle/headers_formatter_spec.rb +0 -25
- data/spec/lib/castle/utils_spec.rb +0 -156
data/lib/castle/errors.rb
CHANGED
@@ -20,6 +20,8 @@ module Castle
|
|
20
20
|
class ConfigurationError < Castle::Error; end
|
21
21
|
# error returned by api
|
22
22
|
class ApiError < Castle::Error; end
|
23
|
+
# webhook signature verification error
|
24
|
+
class WebhookVerificationError < Castle::Error; end
|
23
25
|
|
24
26
|
# api error bad request 400
|
25
27
|
class BadRequestError < Castle::ApiError; end
|
data/lib/castle/events.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
module Castle
|
4
4
|
# list of events based on https://docs.castle.io/api_reference/#list-of-recognized-events
|
5
5
|
module Events
|
6
|
-
# Record when a user
|
6
|
+
# Record when a user successfully logs in.
|
7
7
|
LOGIN_SUCCEEDED = '$login.succeeded'
|
8
8
|
# Record when a user failed to log in.
|
9
9
|
LOGIN_FAILED = '$login.failed'
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module Failover
|
5
|
+
# generate failover authentication response
|
6
|
+
class PrepareResponse
|
7
|
+
def initialize(user_id, reason:, strategy: Castle.config.failover_strategy)
|
8
|
+
@strategy = strategy
|
9
|
+
@reason = reason
|
10
|
+
@user_id = user_id
|
11
|
+
end
|
12
|
+
|
13
|
+
def call
|
14
|
+
{
|
15
|
+
action: @strategy.to_s,
|
16
|
+
user_id: @user_id,
|
17
|
+
failover: true,
|
18
|
+
failover_reason: @reason
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module Failover
|
5
|
+
# handles failover strategy consts
|
6
|
+
module Strategy
|
7
|
+
# allow
|
8
|
+
ALLOW = :allow
|
9
|
+
# deny
|
10
|
+
DENY = :deny
|
11
|
+
# challenge
|
12
|
+
CHALLENGE = :challenge
|
13
|
+
# throw an error
|
14
|
+
THROW = :throw
|
15
|
+
end
|
16
|
+
|
17
|
+
# list of possible strategies
|
18
|
+
STRATEGIES = %i[allow deny challenge throw].freeze
|
19
|
+
end
|
20
|
+
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Castle
|
4
|
-
module
|
4
|
+
module Headers
|
5
5
|
# used for extraction of cookies and headers from the request
|
6
|
-
class
|
6
|
+
class Extract
|
7
7
|
# Headers that we will never scrub, even if they land on the configuration denylist.
|
8
8
|
ALWAYS_ALLOWLISTED = %w[User-Agent].freeze
|
9
9
|
|
@@ -13,9 +13,11 @@ module Castle
|
|
13
13
|
private_constant :ALWAYS_ALLOWLISTED, :ALWAYS_DENYLISTED
|
14
14
|
|
15
15
|
# @param headers [Hash]
|
16
|
-
|
16
|
+
# @param config [Castle::Configuration, Castle::SingletonConfiguration]
|
17
|
+
def initialize(headers, config = Castle.config)
|
17
18
|
@headers = headers
|
18
|
-
@
|
19
|
+
@config = config
|
20
|
+
@no_allowlist = config.allowlisted.empty?
|
19
21
|
end
|
20
22
|
|
21
23
|
# Serialize HTTP headers
|
@@ -35,8 +37,8 @@ module Castle
|
|
35
37
|
def header_value(name, value)
|
36
38
|
return true if ALWAYS_DENYLISTED.include?(name)
|
37
39
|
return value if ALWAYS_ALLOWLISTED.include?(name)
|
38
|
-
return true if
|
39
|
-
return value if @no_allowlist ||
|
40
|
+
return true if @config.denylisted.include?(name)
|
41
|
+
return value if @no_allowlist || @config.allowlisted.include?(name)
|
40
42
|
|
41
43
|
true
|
42
44
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module Headers
|
5
|
+
# used for preparing valuable headers list
|
6
|
+
class Filter
|
7
|
+
# headers filter
|
8
|
+
# HTTP_ - this is how Rack prefixes incoming HTTP headers
|
9
|
+
# CONTENT_LENGTH - for responses without Content-Length or Transfer-Encoding header
|
10
|
+
# REMOTE_ADDR - ip address header returned by web server
|
11
|
+
VALUABLE_HEADERS = /^
|
12
|
+
HTTP(?:_|-).*|
|
13
|
+
CONTENT(?:_|-)LENGTH|
|
14
|
+
REMOTE(?:_|-)ADDR
|
15
|
+
$/xi.freeze
|
16
|
+
|
17
|
+
private_constant :VALUABLE_HEADERS
|
18
|
+
|
19
|
+
# @param request [Rack::Request]
|
20
|
+
def initialize(request)
|
21
|
+
@request_env = request.env
|
22
|
+
@header_format = Castle::Headers::Format
|
23
|
+
end
|
24
|
+
|
25
|
+
# Serialize HTTP headers
|
26
|
+
# @return [Hash]
|
27
|
+
def call
|
28
|
+
@request_env.keys.each_with_object({}) do |header_name, acc|
|
29
|
+
next unless header_name.match(VALUABLE_HEADERS)
|
30
|
+
|
31
|
+
formatted_name = @header_format.call(header_name)
|
32
|
+
acc[formatted_name] = @request_env[header_name]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module Headers
|
5
|
+
# formats header name
|
6
|
+
class Format
|
7
|
+
class << self
|
8
|
+
# @param header [String]
|
9
|
+
# @return [String]
|
10
|
+
def call(header)
|
11
|
+
format(header.to_s.gsub(/^HTTP(?:_|-)/i, ''))
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
# @param header [String]
|
17
|
+
# @return [String]
|
18
|
+
def format(header)
|
19
|
+
header.split(/_|-/).map(&:capitalize).join('-')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Castle
|
4
|
-
module
|
4
|
+
module IP
|
5
5
|
# used for extraction of ip from the request
|
6
|
-
class
|
6
|
+
class Extract
|
7
7
|
# ordered list of ip headers for ip extraction
|
8
8
|
DEFAULT = %w[X-Forwarded-For Remote-Addr].freeze
|
9
9
|
# list of header which are used with proxy depth setting
|
@@ -12,12 +12,13 @@ module Castle
|
|
12
12
|
private_constant :DEFAULT
|
13
13
|
|
14
14
|
# @param headers [Hash]
|
15
|
-
|
15
|
+
# @param config [Castle::Configuration, Castle::SingletonConfiguration]
|
16
|
+
def initialize(headers, config = Castle.config)
|
16
17
|
@headers = headers
|
17
|
-
@ip_headers =
|
18
|
-
@proxies =
|
19
|
-
@trust_proxy_chain =
|
20
|
-
@trusted_proxy_depth =
|
18
|
+
@ip_headers = config.ip_headers.empty? ? DEFAULT : config.ip_headers
|
19
|
+
@proxies = config.trusted_proxies + Castle::Configuration::TRUSTED_PROXIES
|
20
|
+
@trust_proxy_chain = config.trust_proxy_chain
|
21
|
+
@trusted_proxy_depth = config.trusted_proxy_depth
|
21
22
|
end
|
22
23
|
|
23
24
|
# Order of headers:
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
# module for logger handling
|
5
|
+
module Logger
|
6
|
+
class << self
|
7
|
+
# @param message [String]
|
8
|
+
# @param data [String]
|
9
|
+
# @param config [Castle::Configuration, Castle::SingletonConfiguration]
|
10
|
+
def call(message, data = nil, config = Castle.config)
|
11
|
+
logger = config.logger
|
12
|
+
|
13
|
+
return unless logger
|
14
|
+
|
15
|
+
logger.info("[CASTLE] #{message} #{data}".strip)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module Payload
|
5
|
+
# this prepare payload based on the request
|
6
|
+
module Prepare
|
7
|
+
class << self
|
8
|
+
# @param payload_options [Hash]
|
9
|
+
# @param request [Request]
|
10
|
+
# @param options [Hash] required for context preparation
|
11
|
+
# @return [Hash]
|
12
|
+
def call(payload_options, request, options = {})
|
13
|
+
context = Castle::Context::Prepare.call(request, payload_options.merge(options))
|
14
|
+
|
15
|
+
payload = Castle::Utils::DeepSymbolizeKeys.call(
|
16
|
+
payload_options || {}
|
17
|
+
).merge(context: context)
|
18
|
+
payload[:timestamp] ||= Castle::Utils::GetTimestamp.call
|
19
|
+
|
20
|
+
warn '[DEPRECATION] use user_traits instead of traits key' if payload.key?(:traits)
|
21
|
+
|
22
|
+
payload
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/castle/secure_mode.rb
CHANGED
@@ -4,8 +4,12 @@ require 'openssl'
|
|
4
4
|
|
5
5
|
module Castle
|
6
6
|
module SecureMode
|
7
|
-
|
8
|
-
|
7
|
+
class << self
|
8
|
+
# @param user_id [String]
|
9
|
+
# @param config [Castle::Configuration, Castle::SingletonConfiguration]
|
10
|
+
def signature(user_id, config = Castle.config)
|
11
|
+
OpenSSL::HMAC.hexdigest('sha256', config.api_secret, user_id.to_s)
|
12
|
+
end
|
9
13
|
end
|
10
14
|
end
|
11
15
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
# this module uses the Connection object
|
5
|
+
# and provides start method for persistent connection usage
|
6
|
+
# when there is a need of sending multiple requests at once
|
7
|
+
module Session
|
8
|
+
HTTPS_SCHEME = 'https'
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def call(&block)
|
12
|
+
return unless block_given?
|
13
|
+
|
14
|
+
Castle::Core::GetConnection.call.start(&block)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module Utils
|
5
|
+
module CleanInvalidChars
|
6
|
+
class << self
|
7
|
+
def call(arg)
|
8
|
+
case arg
|
9
|
+
when ::String
|
10
|
+
arg.encode('UTF-8', invalid: :replace, undef: :replace)
|
11
|
+
when ::Hash
|
12
|
+
arg.transform_values do |v|
|
13
|
+
Castle::Utils::CleanInvalidChars.call(v)
|
14
|
+
end
|
15
|
+
when ::Array
|
16
|
+
arg.map { |el| Castle::Utils::CleanInvalidChars.call(el) }
|
17
|
+
else
|
18
|
+
arg
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module Utils
|
5
|
+
module DeepSymbolizeKeys
|
6
|
+
class << self
|
7
|
+
# Returns a new hash with all keys converted to symbols, as long as
|
8
|
+
# they respond to +to_sym+. This includes the keys from the root hash
|
9
|
+
# and from all nested hashes and arrays.
|
10
|
+
#
|
11
|
+
# hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
|
12
|
+
#
|
13
|
+
# Castle::Hash.deep_symbolize_keys(hash)
|
14
|
+
# # => {:person=>{:name=>"Rob", :age=>"28"}}
|
15
|
+
def call(object, &block)
|
16
|
+
case object
|
17
|
+
when Hash
|
18
|
+
object.each_with_object({}) do |(key, value), result|
|
19
|
+
result[key.to_sym] = Castle::Utils::DeepSymbolizeKeys.call(value, &block)
|
20
|
+
end
|
21
|
+
when Array
|
22
|
+
object.map { |e| Castle::Utils::DeepSymbolizeKeys.call(e, &block) }
|
23
|
+
else
|
24
|
+
object
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def call!(object, &block)
|
29
|
+
case object
|
30
|
+
when Hash
|
31
|
+
object.each_key do |key|
|
32
|
+
value = object.delete(key)
|
33
|
+
object[key.to_sym] = Castle::Utils::DeepSymbolizeKeys.call!(value, &block)
|
34
|
+
end
|
35
|
+
object
|
36
|
+
when Array
|
37
|
+
object.map! { |e| Castle::Utils::DeepSymbolizeKeys.call!(e, &block) }
|
38
|
+
else
|
39
|
+
object
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module Utils
|
5
|
+
# Generates a timestamp
|
6
|
+
class GetTimestamp
|
7
|
+
class << self
|
8
|
+
# Returns current time as ISO8601 formatted string
|
9
|
+
def call
|
10
|
+
Time.now.utc.iso8601(3)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
module Castle
|
4
4
|
module Utils
|
5
|
-
class
|
5
|
+
class Merge
|
6
6
|
def self.call(base, extra)
|
7
|
-
base_s = Castle::Utils.
|
8
|
-
extra_s = Castle::Utils.
|
7
|
+
base_s = Castle::Utils::DeepSymbolizeKeys.call(base)
|
8
|
+
extra_s = Castle::Utils::DeepSymbolizeKeys.call(extra)
|
9
9
|
|
10
10
|
extra_s.each do |name, value|
|
11
11
|
if value.nil?
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module Utils
|
5
|
+
# Code borrowed from ActiveSupport
|
6
|
+
class SecureCompare
|
7
|
+
class << self
|
8
|
+
# @param str_a [String] first string to be compared
|
9
|
+
# @param str_b [String] second string to be compared
|
10
|
+
def call(str_a, str_b)
|
11
|
+
return false unless str_a.bytesize == str_b.bytesize
|
12
|
+
|
13
|
+
l = str_a.unpack "C#{str_a.bytesize}"
|
14
|
+
|
15
|
+
res = 0
|
16
|
+
str_b.each_byte { |byte| res |= byte ^ l.shift }
|
17
|
+
res.zero?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|