castle-rb 5.0.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|