activesupport 6.1.0 → 7.1.5.1
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/CHANGELOG.md +1075 -325
- data/MIT-LICENSE +1 -1
- data/README.rdoc +7 -7
- data/lib/active_support/actionable_error.rb +4 -2
- data/lib/active_support/array_inquirer.rb +2 -2
- data/lib/active_support/backtrace_cleaner.rb +32 -7
- data/lib/active_support/benchmarkable.rb +3 -2
- data/lib/active_support/broadcast_logger.rb +251 -0
- data/lib/active_support/builder.rb +1 -1
- data/lib/active_support/cache/coder.rb +153 -0
- data/lib/active_support/cache/entry.rb +134 -0
- data/lib/active_support/cache/file_store.rb +53 -20
- data/lib/active_support/cache/mem_cache_store.rb +201 -62
- data/lib/active_support/cache/memory_store.rb +86 -24
- data/lib/active_support/cache/null_store.rb +16 -2
- data/lib/active_support/cache/redis_cache_store.rb +186 -193
- data/lib/active_support/cache/serializer_with_fallback.rb +175 -0
- data/lib/active_support/cache/strategy/local_cache.rb +63 -71
- data/lib/active_support/cache.rb +487 -249
- data/lib/active_support/callbacks.rb +227 -105
- data/lib/active_support/code_generator.rb +70 -0
- data/lib/active_support/concern.rb +9 -7
- data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +44 -7
- data/lib/active_support/concurrency/null_lock.rb +13 -0
- data/lib/active_support/concurrency/share_lock.rb +2 -2
- data/lib/active_support/configurable.rb +18 -5
- data/lib/active_support/configuration_file.rb +7 -2
- data/lib/active_support/core_ext/array/access.rb +1 -5
- data/lib/active_support/core_ext/array/conversions.rb +15 -13
- data/lib/active_support/core_ext/array/grouping.rb +6 -6
- data/lib/active_support/core_ext/array/inquiry.rb +2 -2
- data/lib/active_support/core_ext/big_decimal/conversions.rb +1 -1
- data/lib/active_support/core_ext/class/subclasses.rb +37 -26
- data/lib/active_support/core_ext/date/blank.rb +1 -1
- data/lib/active_support/core_ext/date/calculations.rb +24 -9
- data/lib/active_support/core_ext/date/conversions.rb +16 -15
- data/lib/active_support/core_ext/date_and_time/calculations.rb +14 -4
- data/lib/active_support/core_ext/date_and_time/compatibility.rb +1 -1
- data/lib/active_support/core_ext/date_time/blank.rb +1 -1
- data/lib/active_support/core_ext/date_time/calculations.rb +4 -0
- data/lib/active_support/core_ext/date_time/conversions.rb +19 -15
- data/lib/active_support/core_ext/digest/uuid.rb +30 -13
- data/lib/active_support/core_ext/enumerable.rb +85 -83
- data/lib/active_support/core_ext/erb/util.rb +196 -0
- data/lib/active_support/core_ext/file/atomic.rb +3 -1
- data/lib/active_support/core_ext/hash/conversions.rb +1 -2
- data/lib/active_support/core_ext/hash/deep_merge.rb +22 -14
- data/lib/active_support/core_ext/hash/deep_transform_values.rb +3 -3
- data/lib/active_support/core_ext/hash/indifferent_access.rb +3 -3
- data/lib/active_support/core_ext/hash/keys.rb +4 -4
- data/lib/active_support/core_ext/integer/inflections.rb +12 -12
- data/lib/active_support/core_ext/kernel/reporting.rb +4 -4
- data/lib/active_support/core_ext/kernel/singleton_class.rb +1 -1
- data/lib/active_support/core_ext/module/attribute_accessors.rb +8 -0
- data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +49 -22
- data/lib/active_support/core_ext/module/concerning.rb +6 -6
- data/lib/active_support/core_ext/module/delegation.rb +81 -43
- data/lib/active_support/core_ext/module/deprecation.rb +15 -12
- data/lib/active_support/core_ext/module/introspection.rb +0 -1
- data/lib/active_support/core_ext/name_error.rb +2 -8
- data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
- data/lib/active_support/core_ext/numeric/conversions.rb +82 -77
- data/lib/active_support/core_ext/object/acts_like.rb +29 -5
- data/lib/active_support/core_ext/object/blank.rb +2 -2
- data/lib/active_support/core_ext/object/deep_dup.rb +17 -1
- data/lib/active_support/core_ext/object/duplicable.rb +31 -11
- data/lib/active_support/core_ext/object/inclusion.rb +13 -5
- data/lib/active_support/core_ext/object/instance_variables.rb +22 -12
- data/lib/active_support/core_ext/object/json.rb +49 -27
- data/lib/active_support/core_ext/object/to_query.rb +2 -4
- data/lib/active_support/core_ext/object/try.rb +20 -20
- data/lib/active_support/core_ext/object/with.rb +44 -0
- data/lib/active_support/core_ext/object/with_options.rb +25 -6
- data/lib/active_support/core_ext/object.rb +1 -0
- data/lib/active_support/core_ext/pathname/blank.rb +16 -0
- data/lib/active_support/core_ext/pathname/existence.rb +23 -0
- data/lib/active_support/core_ext/pathname.rb +4 -0
- data/lib/active_support/core_ext/range/compare_range.rb +0 -25
- data/lib/active_support/core_ext/range/conversions.rb +34 -13
- data/lib/active_support/core_ext/range/each.rb +1 -1
- data/lib/active_support/core_ext/range/overlap.rb +40 -0
- data/lib/active_support/core_ext/range.rb +1 -2
- data/lib/active_support/core_ext/securerandom.rb +25 -13
- data/lib/active_support/core_ext/string/conversions.rb +2 -2
- data/lib/active_support/core_ext/string/filters.rb +21 -15
- data/lib/active_support/core_ext/string/indent.rb +1 -1
- data/lib/active_support/core_ext/string/inflections.rb +17 -10
- data/lib/active_support/core_ext/string/inquiry.rb +1 -1
- data/lib/active_support/core_ext/string/output_safety.rb +85 -165
- data/lib/active_support/core_ext/symbol/starts_ends_with.rb +0 -8
- data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
- data/lib/active_support/core_ext/time/calculations.rb +30 -8
- data/lib/active_support/core_ext/time/conversions.rb +15 -13
- data/lib/active_support/core_ext/time/zones.rb +12 -28
- data/lib/active_support/core_ext.rb +2 -1
- data/lib/active_support/current_attributes.rb +47 -20
- data/lib/active_support/deep_mergeable.rb +53 -0
- data/lib/active_support/dependencies/autoload.rb +17 -12
- data/lib/active_support/dependencies/interlock.rb +10 -18
- data/lib/active_support/dependencies/require_dependency.rb +28 -0
- data/lib/active_support/dependencies.rb +58 -788
- data/lib/active_support/deprecation/behaviors.rb +66 -40
- data/lib/active_support/deprecation/constant_accessor.rb +5 -4
- data/lib/active_support/deprecation/deprecators.rb +104 -0
- data/lib/active_support/deprecation/disallowed.rb +6 -8
- data/lib/active_support/deprecation/instance_delegator.rb +31 -4
- data/lib/active_support/deprecation/method_wrappers.rb +9 -26
- data/lib/active_support/deprecation/proxy_wrappers.rb +38 -23
- data/lib/active_support/deprecation/reporting.rb +43 -26
- data/lib/active_support/deprecation.rb +32 -5
- data/lib/active_support/deprecator.rb +7 -0
- data/lib/active_support/descendants_tracker.rb +150 -72
- data/lib/active_support/digest.rb +5 -3
- data/lib/active_support/duration/iso8601_parser.rb +3 -3
- data/lib/active_support/duration/iso8601_serializer.rb +9 -3
- data/lib/active_support/duration.rb +83 -52
- data/lib/active_support/encrypted_configuration.rb +72 -9
- data/lib/active_support/encrypted_file.rb +29 -13
- data/lib/active_support/environment_inquirer.rb +23 -3
- data/lib/active_support/error_reporter/test_helper.rb +15 -0
- data/lib/active_support/error_reporter.rb +203 -0
- data/lib/active_support/evented_file_update_checker.rb +20 -7
- data/lib/active_support/execution_context/test_helper.rb +13 -0
- data/lib/active_support/execution_context.rb +53 -0
- data/lib/active_support/execution_wrapper.rb +44 -22
- data/lib/active_support/executor/test_helper.rb +7 -0
- data/lib/active_support/file_update_checker.rb +4 -2
- data/lib/active_support/fork_tracker.rb +28 -11
- data/lib/active_support/gem_version.rb +4 -4
- data/lib/active_support/gzip.rb +2 -0
- data/lib/active_support/hash_with_indifferent_access.rb +44 -19
- data/lib/active_support/html_safe_translation.rb +53 -0
- data/lib/active_support/i18n.rb +2 -1
- data/lib/active_support/i18n_railtie.rb +21 -14
- data/lib/active_support/inflector/inflections.rb +25 -7
- data/lib/active_support/inflector/methods.rb +50 -64
- data/lib/active_support/inflector/transliterate.rb +4 -2
- data/lib/active_support/isolated_execution_state.rb +76 -0
- data/lib/active_support/json/decoding.rb +2 -1
- data/lib/active_support/json/encoding.rb +27 -45
- data/lib/active_support/key_generator.rb +31 -6
- data/lib/active_support/lazy_load_hooks.rb +33 -7
- data/lib/active_support/locale/en.yml +4 -2
- data/lib/active_support/log_subscriber/test_helper.rb +2 -2
- data/lib/active_support/log_subscriber.rb +97 -35
- data/lib/active_support/logger.rb +9 -60
- data/lib/active_support/logger_thread_safe_level.rb +11 -34
- data/lib/active_support/message_encryptor.rb +206 -56
- data/lib/active_support/message_encryptors.rb +141 -0
- data/lib/active_support/message_pack/cache_serializer.rb +23 -0
- data/lib/active_support/message_pack/extensions.rb +292 -0
- data/lib/active_support/message_pack/serializer.rb +63 -0
- data/lib/active_support/message_pack.rb +50 -0
- data/lib/active_support/message_verifier.rb +235 -84
- data/lib/active_support/message_verifiers.rb +135 -0
- data/lib/active_support/messages/codec.rb +65 -0
- data/lib/active_support/messages/metadata.rb +112 -46
- data/lib/active_support/messages/rotation_coordinator.rb +93 -0
- data/lib/active_support/messages/rotator.rb +34 -32
- data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
- data/lib/active_support/multibyte/chars.rb +12 -11
- data/lib/active_support/multibyte/unicode.rb +9 -49
- data/lib/active_support/multibyte.rb +1 -1
- data/lib/active_support/notifications/fanout.rb +304 -114
- data/lib/active_support/notifications/instrumenter.rb +117 -35
- data/lib/active_support/notifications.rb +25 -25
- data/lib/active_support/number_helper/number_converter.rb +14 -7
- data/lib/active_support/number_helper/number_to_currency_converter.rb +11 -6
- data/lib/active_support/number_helper/number_to_delimited_converter.rb +1 -1
- data/lib/active_support/number_helper/number_to_human_size_converter.rb +4 -4
- data/lib/active_support/number_helper/number_to_phone_converter.rb +2 -1
- data/lib/active_support/number_helper/number_to_rounded_converter.rb +10 -6
- data/lib/active_support/number_helper/rounding_helper.rb +2 -6
- data/lib/active_support/number_helper.rb +379 -319
- data/lib/active_support/option_merger.rb +10 -18
- data/lib/active_support/ordered_hash.rb +4 -4
- data/lib/active_support/ordered_options.rb +15 -1
- data/lib/active_support/parameter_filter.rb +105 -81
- data/lib/active_support/proxy_object.rb +2 -0
- data/lib/active_support/railtie.rb +83 -21
- data/lib/active_support/reloader.rb +13 -5
- data/lib/active_support/rescuable.rb +18 -16
- data/lib/active_support/ruby_features.rb +7 -0
- data/lib/active_support/secure_compare_rotator.rb +18 -11
- data/lib/active_support/security_utils.rb +1 -1
- data/lib/active_support/string_inquirer.rb +3 -3
- data/lib/active_support/subscriber.rb +11 -40
- data/lib/active_support/syntax_error_proxy.rb +60 -0
- data/lib/active_support/tagged_logging.rb +65 -25
- data/lib/active_support/test_case.rb +166 -27
- data/lib/active_support/testing/assertions.rb +61 -15
- data/lib/active_support/testing/autorun.rb +0 -2
- data/lib/active_support/testing/constant_stubbing.rb +32 -0
- data/lib/active_support/testing/deprecation.rb +53 -2
- data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
- data/lib/active_support/testing/isolation.rb +30 -29
- data/lib/active_support/testing/method_call_assertions.rb +24 -11
- data/lib/active_support/testing/parallelization/server.rb +4 -0
- data/lib/active_support/testing/parallelization/worker.rb +3 -0
- data/lib/active_support/testing/parallelization.rb +4 -0
- data/lib/active_support/testing/parallelize_executor.rb +81 -0
- data/lib/active_support/testing/setup_and_teardown.rb +2 -0
- data/lib/active_support/testing/stream.rb +4 -6
- data/lib/active_support/testing/strict_warnings.rb +39 -0
- data/lib/active_support/testing/tagged_logging.rb +1 -1
- data/lib/active_support/testing/time_helpers.rb +49 -16
- data/lib/active_support/time_with_zone.rb +39 -28
- data/lib/active_support/values/time_zone.rb +50 -18
- data/lib/active_support/version.rb +1 -1
- data/lib/active_support/xml_mini/jdom.rb +4 -11
- data/lib/active_support/xml_mini/libxml.rb +5 -5
- data/lib/active_support/xml_mini/libxmlsax.rb +1 -1
- data/lib/active_support/xml_mini/nokogiri.rb +5 -5
- data/lib/active_support/xml_mini/nokogirisax.rb +2 -2
- data/lib/active_support/xml_mini/rexml.rb +2 -2
- data/lib/active_support/xml_mini.rb +7 -6
- data/lib/active_support.rb +28 -1
- metadata +150 -18
- data/lib/active_support/core_ext/marshal.rb +0 -26
- data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -28
- data/lib/active_support/core_ext/range/overlaps.rb +0 -10
- data/lib/active_support/core_ext/uri.rb +0 -29
- data/lib/active_support/dependencies/zeitwerk_integration.rb +0 -117
- data/lib/active_support/per_thread_registry.rb +0 -60
@@ -1,85 +1,82 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "openssl"
|
3
4
|
require "base64"
|
4
5
|
require "active_support/core_ext/object/blank"
|
5
6
|
require "active_support/security_utils"
|
6
|
-
require "active_support/messages/
|
7
|
+
require "active_support/messages/codec"
|
7
8
|
require "active_support/messages/rotator"
|
8
9
|
|
9
10
|
module ActiveSupport
|
11
|
+
# = Active Support Message Verifier
|
12
|
+
#
|
10
13
|
# +MessageVerifier+ makes it easy to generate and verify messages which are
|
11
14
|
# signed to prevent tampering.
|
12
15
|
#
|
16
|
+
# In a \Rails application, you can use +Rails.application.message_verifier+
|
17
|
+
# to manage unique instances of verifiers for each use case.
|
18
|
+
# {Learn more}[link:classes/Rails/Application.html#method-i-message_verifier].
|
19
|
+
#
|
13
20
|
# This is useful for cases like remember-me tokens and auto-unsubscribe links
|
14
21
|
# where the session store isn't suitable or available.
|
15
22
|
#
|
16
|
-
#
|
17
|
-
# cookies[:remember_me] =
|
23
|
+
# First, generate a signed message:
|
24
|
+
# cookies[:remember_me] = Rails.application.message_verifier(:remember_me).generate([@user.id, 2.weeks.from_now])
|
18
25
|
#
|
19
|
-
#
|
26
|
+
# Later verify that message:
|
20
27
|
#
|
21
|
-
# id, time =
|
22
|
-
# if
|
28
|
+
# id, time = Rails.application.message_verifier(:remember_me).verify(cookies[:remember_me])
|
29
|
+
# if time.future?
|
23
30
|
# self.current_user = User.find(id)
|
24
31
|
# end
|
25
32
|
#
|
26
|
-
#
|
27
|
-
# another serialization method, you can set the serializer in the options
|
28
|
-
# hash upon initialization:
|
29
|
-
#
|
30
|
-
# @verifier = ActiveSupport::MessageVerifier.new('s3Krit', serializer: YAML)
|
31
|
-
#
|
32
|
-
# +MessageVerifier+ creates HMAC signatures using SHA1 hash algorithm by default.
|
33
|
-
# If you want to use a different hash algorithm, you can change it by providing
|
34
|
-
# +:digest+ key as an option while initializing the verifier:
|
33
|
+
# === Confine messages to a specific purpose
|
35
34
|
#
|
36
|
-
#
|
35
|
+
# It's not recommended to use the same verifier for different purposes in your application.
|
36
|
+
# Doing so could allow a malicious actor to re-use a signed message to perform an unauthorized
|
37
|
+
# action.
|
38
|
+
# You can reduce this risk by confining signed messages to a specific +:purpose+.
|
37
39
|
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
# By default any message can be used throughout your app. But they can also be
|
41
|
-
# confined to a specific +:purpose+.
|
42
|
-
#
|
43
|
-
# token = @verifier.generate("this is the chair", purpose: :login)
|
40
|
+
# token = @verifier.generate("signed message", purpose: :login)
|
44
41
|
#
|
45
42
|
# Then that same purpose must be passed when verifying to get the data back out:
|
46
43
|
#
|
47
|
-
# @verifier.verified(token, purpose: :login) # => "
|
44
|
+
# @verifier.verified(token, purpose: :login) # => "signed message"
|
48
45
|
# @verifier.verified(token, purpose: :shipping) # => nil
|
49
46
|
# @verifier.verified(token) # => nil
|
50
47
|
#
|
51
|
-
# @verifier.verify(token, purpose: :login) # => "
|
52
|
-
# @verifier.verify(token, purpose: :shipping) # => ActiveSupport::MessageVerifier::InvalidSignature
|
53
|
-
# @verifier.verify(token) # => ActiveSupport::MessageVerifier::InvalidSignature
|
48
|
+
# @verifier.verify(token, purpose: :login) # => "signed message"
|
49
|
+
# @verifier.verify(token, purpose: :shipping) # => raises ActiveSupport::MessageVerifier::InvalidSignature
|
50
|
+
# @verifier.verify(token) # => raises ActiveSupport::MessageVerifier::InvalidSignature
|
54
51
|
#
|
55
52
|
# Likewise, if a message has no purpose it won't be returned when verifying with
|
56
53
|
# a specific purpose.
|
57
54
|
#
|
58
|
-
# token = @verifier.generate("
|
59
|
-
# @verifier.verified(token, purpose: :
|
60
|
-
# @verifier.verified(token)
|
55
|
+
# token = @verifier.generate("signed message")
|
56
|
+
# @verifier.verified(token, purpose: :redirect) # => nil
|
57
|
+
# @verifier.verified(token) # => "signed message"
|
61
58
|
#
|
62
|
-
# @verifier.verify(token, purpose: :
|
63
|
-
# @verifier.verify(token)
|
59
|
+
# @verifier.verify(token, purpose: :redirect) # => raises ActiveSupport::MessageVerifier::InvalidSignature
|
60
|
+
# @verifier.verify(token) # => "signed message"
|
64
61
|
#
|
65
|
-
# ===
|
62
|
+
# === Expiring messages
|
66
63
|
#
|
67
64
|
# By default messages last forever and verifying one year from now will still
|
68
65
|
# return the original value. But messages can be set to expire at a given
|
69
66
|
# time with +:expires_in+ or +:expires_at+.
|
70
67
|
#
|
71
|
-
# @verifier.generate(
|
72
|
-
# @verifier.generate(
|
68
|
+
# @verifier.generate("signed message", expires_in: 1.month)
|
69
|
+
# @verifier.generate("signed message", expires_at: Time.now.end_of_year)
|
73
70
|
#
|
74
|
-
#
|
71
|
+
# Messages can then be verified and returned until expiry.
|
75
72
|
# Thereafter, the +verified+ method returns +nil+ while +verify+ raises
|
76
|
-
#
|
73
|
+
# +ActiveSupport::MessageVerifier::InvalidSignature+.
|
77
74
|
#
|
78
75
|
# === Rotating keys
|
79
76
|
#
|
80
77
|
# MessageVerifier also supports rotating out old configurations by falling
|
81
|
-
# back to a stack of verifiers. Call +rotate+ to build and add a verifier
|
82
|
-
#
|
78
|
+
# back to a stack of verifiers. Call +rotate+ to build and add a verifier so
|
79
|
+
# either +verified+ or +verify+ will also try verifying with the fallback.
|
83
80
|
#
|
84
81
|
# By default any rotated verifiers use the values of the primary
|
85
82
|
# verifier unless specified otherwise.
|
@@ -91,51 +88,98 @@ module ActiveSupport
|
|
91
88
|
# Then gradually rotate the old values out by adding them as fallbacks. Any message
|
92
89
|
# generated with the old values will then work until the rotation is removed.
|
93
90
|
#
|
94
|
-
# verifier.rotate
|
95
|
-
# verifier.rotate
|
96
|
-
# verifier.rotate
|
91
|
+
# verifier.rotate(old_secret) # Fallback to an old secret instead of @secret.
|
92
|
+
# verifier.rotate(digest: "SHA256") # Fallback to an old digest instead of SHA512.
|
93
|
+
# verifier.rotate(serializer: Marshal) # Fallback to an old serializer instead of JSON.
|
97
94
|
#
|
98
95
|
# Though the above would most likely be combined into one rotation:
|
99
96
|
#
|
100
|
-
# verifier.rotate
|
101
|
-
class MessageVerifier
|
102
|
-
prepend Messages::Rotator
|
97
|
+
# verifier.rotate(old_secret, digest: "SHA256", serializer: Marshal)
|
98
|
+
class MessageVerifier < Messages::Codec
|
99
|
+
prepend Messages::Rotator
|
103
100
|
|
104
101
|
class InvalidSignature < StandardError; end
|
105
102
|
|
106
|
-
|
103
|
+
SEPARATOR = "--" # :nodoc:
|
104
|
+
SEPARATOR_LENGTH = SEPARATOR.length # :nodoc:
|
105
|
+
|
106
|
+
# Initialize a new MessageVerifier with a secret for the signature.
|
107
|
+
#
|
108
|
+
# ==== Options
|
109
|
+
#
|
110
|
+
# [+:digest+]
|
111
|
+
# Digest used for signing. The default is <tt>"SHA1"</tt>. See
|
112
|
+
# +OpenSSL::Digest+ for alternatives.
|
113
|
+
#
|
114
|
+
# [+:serializer+]
|
115
|
+
# The serializer used to serialize message data. You can specify any
|
116
|
+
# object that responds to +dump+ and +load+, or you can choose from
|
117
|
+
# several preconfigured serializers: +:marshal+, +:json_allow_marshal+,
|
118
|
+
# +:json+, +:message_pack_allow_marshal+, +:message_pack+.
|
119
|
+
#
|
120
|
+
# The preconfigured serializers include a fallback mechanism to support
|
121
|
+
# multiple deserialization formats. For example, the +:marshal+ serializer
|
122
|
+
# will serialize using +Marshal+, but can deserialize using +Marshal+,
|
123
|
+
# ActiveSupport::JSON, or ActiveSupport::MessagePack. This makes it easy
|
124
|
+
# to migrate between serializers.
|
125
|
+
#
|
126
|
+
# The +:marshal+, +:json_allow_marshal+, and +:message_pack_allow_marshal+
|
127
|
+
# serializers support deserializing using +Marshal+, but the others do
|
128
|
+
# not. Beware that +Marshal+ is a potential vector for deserialization
|
129
|
+
# attacks in cases where a message signing secret has been leaked. <em>If
|
130
|
+
# possible, choose a serializer that does not support +Marshal+.</em>
|
131
|
+
#
|
132
|
+
# The +:message_pack+ and +:message_pack_allow_marshal+ serializers use
|
133
|
+
# ActiveSupport::MessagePack, which can roundtrip some Ruby types that are
|
134
|
+
# not supported by JSON, and may provide improved performance. However,
|
135
|
+
# these require the +msgpack+ gem.
|
136
|
+
#
|
137
|
+
# When using \Rails, the default depends on +config.active_support.message_serializer+.
|
138
|
+
# Otherwise, the default is +:marshal+.
|
139
|
+
#
|
140
|
+
# [+:url_safe+]
|
141
|
+
# By default, MessageVerifier generates RFC 4648 compliant strings which are
|
142
|
+
# not URL-safe. In other words, they can contain "+" and "/". If you want to
|
143
|
+
# generate URL-safe strings (in compliance with "Base 64 Encoding with URL
|
144
|
+
# and Filename Safe Alphabet" in RFC 4648), you can pass +true+.
|
145
|
+
#
|
146
|
+
# [+:force_legacy_metadata_serializer+]
|
147
|
+
# Whether to use the legacy metadata serializer, which serializes the
|
148
|
+
# message first, then wraps it in an envelope which is also serialized. This
|
149
|
+
# was the default in \Rails 7.0 and below.
|
150
|
+
#
|
151
|
+
# If you don't pass a truthy value, the default is set using
|
152
|
+
# +config.active_support.use_message_serializer_for_metadata+.
|
153
|
+
def initialize(secret, **options)
|
107
154
|
raise ArgumentError, "Secret should not be nil." unless secret
|
155
|
+
super(**options)
|
108
156
|
@secret = secret
|
109
|
-
@digest = digest || "SHA1"
|
110
|
-
@serializer = serializer || Marshal
|
157
|
+
@digest = options[:digest]&.to_s || "SHA1"
|
111
158
|
end
|
112
159
|
|
113
160
|
# Checks if a signed message could have been generated by signing an object
|
114
161
|
# with the +MessageVerifier+'s secret.
|
115
162
|
#
|
116
|
-
# verifier = ActiveSupport::MessageVerifier.new
|
117
|
-
# signed_message = verifier.generate
|
163
|
+
# verifier = ActiveSupport::MessageVerifier.new("secret")
|
164
|
+
# signed_message = verifier.generate("signed message")
|
118
165
|
# verifier.valid_message?(signed_message) # => true
|
119
166
|
#
|
120
167
|
# tampered_message = signed_message.chop # editing the message invalidates the signature
|
121
168
|
# verifier.valid_message?(tampered_message) # => false
|
122
|
-
def valid_message?(
|
123
|
-
|
124
|
-
|
125
|
-
data, digest = signed_message.split("--")
|
126
|
-
data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data))
|
169
|
+
def valid_message?(message)
|
170
|
+
!!catch_and_ignore(:invalid_message_format) { extract_encoded(message) }
|
127
171
|
end
|
128
172
|
|
129
173
|
# Decodes the signed message using the +MessageVerifier+'s secret.
|
130
174
|
#
|
131
|
-
# verifier = ActiveSupport::MessageVerifier.new
|
175
|
+
# verifier = ActiveSupport::MessageVerifier.new("secret")
|
132
176
|
#
|
133
|
-
# signed_message = verifier.generate
|
134
|
-
# verifier.verified(signed_message) # =>
|
177
|
+
# signed_message = verifier.generate("signed message")
|
178
|
+
# verifier.verified(signed_message) # => "signed message"
|
135
179
|
#
|
136
180
|
# Returns +nil+ if the message was not signed with the same secret.
|
137
181
|
#
|
138
|
-
# other_verifier = ActiveSupport::MessageVerifier.new
|
182
|
+
# other_verifier = ActiveSupport::MessageVerifier.new("different_secret")
|
139
183
|
# other_verifier.verified(signed_message) # => nil
|
140
184
|
#
|
141
185
|
# Returns +nil+ if the message is not Base64-encoded.
|
@@ -147,33 +191,68 @@ module ActiveSupport
|
|
147
191
|
#
|
148
192
|
# incompatible_message = "test--dad7b06c94abba8d46a15fafaef56c327665d5ff"
|
149
193
|
# verifier.verified(incompatible_message) # => TypeError: incompatible marshal file format
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
194
|
+
#
|
195
|
+
# ==== Options
|
196
|
+
#
|
197
|
+
# [+:purpose+]
|
198
|
+
# The purpose that the message was generated with. If the purpose does not
|
199
|
+
# match, +verified+ will return +nil+.
|
200
|
+
#
|
201
|
+
# message = verifier.generate("hello", purpose: "greeting")
|
202
|
+
# verifier.verified(message, purpose: "greeting") # => "hello"
|
203
|
+
# verifier.verified(message, purpose: "chatting") # => nil
|
204
|
+
# verifier.verified(message) # => nil
|
205
|
+
#
|
206
|
+
# message = verifier.generate("bye")
|
207
|
+
# verifier.verified(message) # => "bye"
|
208
|
+
# verifier.verified(message, purpose: "greeting") # => nil
|
209
|
+
#
|
210
|
+
def verified(message, **options)
|
211
|
+
catch_and_ignore :invalid_message_format do
|
212
|
+
catch_and_raise :invalid_message_serialization do
|
213
|
+
catch_and_ignore :invalid_message_content do
|
214
|
+
read_message(message, **options)
|
215
|
+
end
|
159
216
|
end
|
160
217
|
end
|
161
218
|
end
|
162
219
|
|
163
220
|
# Decodes the signed message using the +MessageVerifier+'s secret.
|
164
221
|
#
|
165
|
-
# verifier = ActiveSupport::MessageVerifier.new
|
166
|
-
# signed_message = verifier.generate
|
222
|
+
# verifier = ActiveSupport::MessageVerifier.new("secret")
|
223
|
+
# signed_message = verifier.generate("signed message")
|
167
224
|
#
|
168
|
-
# verifier.verify(signed_message) # =>
|
225
|
+
# verifier.verify(signed_message) # => "signed message"
|
169
226
|
#
|
170
227
|
# Raises +InvalidSignature+ if the message was not signed with the same
|
171
228
|
# secret or was not Base64-encoded.
|
172
229
|
#
|
173
|
-
# other_verifier = ActiveSupport::MessageVerifier.new
|
230
|
+
# other_verifier = ActiveSupport::MessageVerifier.new("different_secret")
|
174
231
|
# other_verifier.verify(signed_message) # => ActiveSupport::MessageVerifier::InvalidSignature
|
175
|
-
|
176
|
-
|
232
|
+
#
|
233
|
+
# ==== Options
|
234
|
+
#
|
235
|
+
# [+:purpose+]
|
236
|
+
# The purpose that the message was generated with. If the purpose does not
|
237
|
+
# match, +verify+ will raise ActiveSupport::MessageVerifier::InvalidSignature.
|
238
|
+
#
|
239
|
+
# message = verifier.generate("hello", purpose: "greeting")
|
240
|
+
# verifier.verify(message, purpose: "greeting") # => "hello"
|
241
|
+
# verifier.verify(message, purpose: "chatting") # => raises InvalidSignature
|
242
|
+
# verifier.verify(message) # => raises InvalidSignature
|
243
|
+
#
|
244
|
+
# message = verifier.generate("bye")
|
245
|
+
# verifier.verify(message) # => "bye"
|
246
|
+
# verifier.verify(message, purpose: "greeting") # => raises InvalidSignature
|
247
|
+
#
|
248
|
+
def verify(message, **options)
|
249
|
+
catch_and_raise :invalid_message_format, as: InvalidSignature do
|
250
|
+
catch_and_raise :invalid_message_serialization do
|
251
|
+
catch_and_raise :invalid_message_content, as: InvalidSignature do
|
252
|
+
read_message(message, **options)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
177
256
|
end
|
178
257
|
|
179
258
|
# Generates a signed message for the provided value.
|
@@ -181,25 +260,97 @@ module ActiveSupport
|
|
181
260
|
# The message is signed with the +MessageVerifier+'s secret.
|
182
261
|
# Returns Base64-encoded message joined with the generated signature.
|
183
262
|
#
|
184
|
-
# verifier = ActiveSupport::MessageVerifier.new
|
185
|
-
# verifier.generate
|
186
|
-
|
187
|
-
|
188
|
-
|
263
|
+
# verifier = ActiveSupport::MessageVerifier.new("secret")
|
264
|
+
# verifier.generate("signed message") # => "BAhJIhNzaWduZWQgbWVzc2FnZQY6BkVU--f67d5f27c3ee0b8483cebf2103757455e947493b"
|
265
|
+
#
|
266
|
+
# ==== Options
|
267
|
+
#
|
268
|
+
# [+:expires_at+]
|
269
|
+
# The datetime at which the message expires. After this datetime,
|
270
|
+
# verification of the message will fail.
|
271
|
+
#
|
272
|
+
# message = verifier.generate("hello", expires_at: Time.now.tomorrow)
|
273
|
+
# verifier.verified(message) # => "hello"
|
274
|
+
# # 24 hours later...
|
275
|
+
# verifier.verified(message) # => nil
|
276
|
+
# verifier.verify(message) # => raises ActiveSupport::MessageVerifier::InvalidSignature
|
277
|
+
#
|
278
|
+
# [+:expires_in+]
|
279
|
+
# The duration for which the message is valid. After this duration has
|
280
|
+
# elapsed, verification of the message will fail.
|
281
|
+
#
|
282
|
+
# message = verifier.generate("hello", expires_in: 24.hours)
|
283
|
+
# verifier.verified(message) # => "hello"
|
284
|
+
# # 24 hours later...
|
285
|
+
# verifier.verified(message) # => nil
|
286
|
+
# verifier.verify(message) # => raises ActiveSupport::MessageVerifier::InvalidSignature
|
287
|
+
#
|
288
|
+
# [+:purpose+]
|
289
|
+
# The purpose of the message. If specified, the same purpose must be
|
290
|
+
# specified when verifying the message; otherwise, verification will fail.
|
291
|
+
# (See #verified and #verify.)
|
292
|
+
def generate(value, **options)
|
293
|
+
create_message(value, **options)
|
294
|
+
end
|
295
|
+
|
296
|
+
def create_message(value, **options) # :nodoc:
|
297
|
+
sign_encoded(encode(serialize_with_metadata(value, **options)))
|
298
|
+
end
|
299
|
+
|
300
|
+
def read_message(message, **options) # :nodoc:
|
301
|
+
deserialize_with_metadata(decode(extract_encoded(message)), **options)
|
302
|
+
end
|
303
|
+
|
304
|
+
def inspect # :nodoc:
|
305
|
+
"#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>"
|
189
306
|
end
|
190
307
|
|
191
308
|
private
|
192
|
-
def
|
193
|
-
|
309
|
+
def sign_encoded(encoded)
|
310
|
+
digest = generate_digest(encoded)
|
311
|
+
encoded << SEPARATOR << digest
|
194
312
|
end
|
195
313
|
|
196
|
-
def
|
197
|
-
|
314
|
+
def extract_encoded(signed)
|
315
|
+
if signed.nil? || !signed.valid_encoding?
|
316
|
+
throw :invalid_message_format, "invalid message string"
|
317
|
+
end
|
318
|
+
|
319
|
+
if separator_index = separator_index_for(signed)
|
320
|
+
encoded = signed[0, separator_index]
|
321
|
+
digest = signed[separator_index + SEPARATOR_LENGTH, digest_length_in_hex]
|
322
|
+
end
|
323
|
+
|
324
|
+
unless digest_matches_data?(digest, encoded)
|
325
|
+
throw :invalid_message_format, "mismatched digest"
|
326
|
+
end
|
327
|
+
|
328
|
+
encoded
|
198
329
|
end
|
199
330
|
|
200
331
|
def generate_digest(data)
|
201
|
-
|
202
|
-
|
332
|
+
OpenSSL::HMAC.hexdigest(@digest, @secret, data)
|
333
|
+
end
|
334
|
+
|
335
|
+
def digest_length_in_hex
|
336
|
+
# In hexadecimal (AKA base16) it takes 4 bits to represent a character,
|
337
|
+
# hence we multiply the digest's length (in bytes) by 8 to get it in
|
338
|
+
# bits and divide by 4 to get its number of characters it hex. Well, 8
|
339
|
+
# divided by 4 is 2.
|
340
|
+
@digest_length_in_hex ||= OpenSSL::Digest.new(@digest).digest_length * 2
|
341
|
+
end
|
342
|
+
|
343
|
+
def separator_at?(signed_message, index)
|
344
|
+
signed_message[index, SEPARATOR_LENGTH] == SEPARATOR
|
345
|
+
end
|
346
|
+
|
347
|
+
def separator_index_for(signed_message)
|
348
|
+
index = signed_message.length - digest_length_in_hex - SEPARATOR_LENGTH
|
349
|
+
index unless index.negative? || !separator_at?(signed_message, index)
|
350
|
+
end
|
351
|
+
|
352
|
+
def digest_matches_data?(digest, data)
|
353
|
+
data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data))
|
203
354
|
end
|
204
355
|
end
|
205
356
|
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/messages/rotation_coordinator"
|
4
|
+
|
5
|
+
module ActiveSupport
|
6
|
+
class MessageVerifiers < Messages::RotationCoordinator
|
7
|
+
##
|
8
|
+
# :attr_accessor: transitional
|
9
|
+
#
|
10
|
+
# If true, the first two rotation option sets are swapped when building
|
11
|
+
# message verifiers. For example, with the following configuration, message
|
12
|
+
# verifiers will generate messages using <tt>serializer: Marshal, url_safe: true</tt>,
|
13
|
+
# and will able to verify messages that were generated using any of the
|
14
|
+
# three option sets:
|
15
|
+
#
|
16
|
+
# verifiers = ActiveSupport::MessageVerifiers.new { ... }
|
17
|
+
# verifiers.rotate(serializer: JSON, url_safe: true)
|
18
|
+
# verifiers.rotate(serializer: Marshal, url_safe: true)
|
19
|
+
# verifiers.rotate(serializer: Marshal, url_safe: false)
|
20
|
+
# verifiers.transitional = true
|
21
|
+
#
|
22
|
+
# This can be useful when performing a rolling deploy of an application,
|
23
|
+
# wherein servers that have not yet been updated must still be able to
|
24
|
+
# verify messages from updated servers. In such a scenario, first perform a
|
25
|
+
# rolling deploy with the new rotation (e.g. <tt>serializer: JSON, url_safe: true</tt>)
|
26
|
+
# as the first rotation and <tt>transitional = true</tt>. Then, after all
|
27
|
+
# servers have been updated, perform a second rolling deploy with
|
28
|
+
# <tt>transitional = false</tt>.
|
29
|
+
|
30
|
+
##
|
31
|
+
# :method: initialize
|
32
|
+
# :call-seq: initialize(&secret_generator)
|
33
|
+
#
|
34
|
+
# Initializes a new instance. +secret_generator+ must accept a salt, and
|
35
|
+
# return a suitable secret (string). +secret_generator+ may also accept
|
36
|
+
# arbitrary kwargs. If #rotate is called with any options matching those
|
37
|
+
# kwargs, those options will be passed to +secret_generator+ instead of to
|
38
|
+
# the message verifier.
|
39
|
+
#
|
40
|
+
# verifiers = ActiveSupport::MessageVerifiers.new do |salt, base:|
|
41
|
+
# MySecretGenerator.new(base).generate(salt)
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# verifiers.rotate(base: "...")
|
45
|
+
|
46
|
+
##
|
47
|
+
# :method: []
|
48
|
+
# :call-seq: [](salt)
|
49
|
+
#
|
50
|
+
# Returns a MessageVerifier configured with a secret derived from the
|
51
|
+
# given +salt+, and options from #rotate. MessageVerifier instances will
|
52
|
+
# be memoized, so the same +salt+ will return the same instance.
|
53
|
+
|
54
|
+
##
|
55
|
+
# :method: []=
|
56
|
+
# :call-seq: []=(salt, verifier)
|
57
|
+
#
|
58
|
+
# Overrides a MessageVerifier instance associated with a given +salt+.
|
59
|
+
|
60
|
+
##
|
61
|
+
# :method: rotate
|
62
|
+
# :call-seq: rotate(**options)
|
63
|
+
#
|
64
|
+
# Adds +options+ to the list of option sets. Messages will be signed using
|
65
|
+
# the first set in the list. When verifying, however, each set will be
|
66
|
+
# tried, in order, until one succeeds.
|
67
|
+
#
|
68
|
+
# Notably, the +:secret_generator+ option can specify a different secret
|
69
|
+
# generator than the one initially specified. The secret generator must
|
70
|
+
# respond to +call+, accept a salt, and return a suitable secret (string).
|
71
|
+
# The secret generator may also accept arbitrary kwargs.
|
72
|
+
#
|
73
|
+
# If any options match the kwargs of the operative secret generator, those
|
74
|
+
# options will be passed to the secret generator instead of to the message
|
75
|
+
# verifier.
|
76
|
+
#
|
77
|
+
# For fine-grained per-salt rotations, a block form is supported. The block
|
78
|
+
# will receive the salt, and should return an appropriate options Hash. The
|
79
|
+
# block may also return +nil+ to indicate that the rotation does not apply
|
80
|
+
# to the given salt. For example:
|
81
|
+
#
|
82
|
+
# verifiers = ActiveSupport::MessageVerifiers.new { ... }
|
83
|
+
#
|
84
|
+
# verifiers.rotate do |salt|
|
85
|
+
# case salt
|
86
|
+
# when :foo
|
87
|
+
# { serializer: JSON, url_safe: true }
|
88
|
+
# when :bar
|
89
|
+
# { serializer: Marshal, url_safe: true }
|
90
|
+
# end
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# verifiers.rotate(serializer: Marshal, url_safe: false)
|
94
|
+
#
|
95
|
+
# # Uses `serializer: JSON, url_safe: true`.
|
96
|
+
# # Falls back to `serializer: Marshal, url_safe: false`.
|
97
|
+
# verifiers[:foo]
|
98
|
+
#
|
99
|
+
# # Uses `serializer: Marshal, url_safe: true`.
|
100
|
+
# # Falls back to `serializer: Marshal, url_safe: false`.
|
101
|
+
# verifiers[:bar]
|
102
|
+
#
|
103
|
+
# # Uses `serializer: Marshal, url_safe: false`.
|
104
|
+
# verifiers[:baz]
|
105
|
+
|
106
|
+
##
|
107
|
+
# :method: rotate_defaults
|
108
|
+
# :call-seq: rotate_defaults
|
109
|
+
#
|
110
|
+
# Invokes #rotate with the default options.
|
111
|
+
|
112
|
+
##
|
113
|
+
# :method: clear_rotations
|
114
|
+
# :call-seq: clear_rotations
|
115
|
+
#
|
116
|
+
# Clears the list of option sets.
|
117
|
+
|
118
|
+
##
|
119
|
+
# :method: on_rotation
|
120
|
+
# :call-seq: on_rotation(&callback)
|
121
|
+
#
|
122
|
+
# Sets a callback to invoke when a message is verified using an option set
|
123
|
+
# other than the first.
|
124
|
+
#
|
125
|
+
# For example, this callback could log each time it is called, and thus
|
126
|
+
# indicate whether old option sets are still in use or can be removed from
|
127
|
+
# rotation.
|
128
|
+
|
129
|
+
##
|
130
|
+
private
|
131
|
+
def build(salt, secret_generator:, secret_generator_options:, **options)
|
132
|
+
MessageVerifier.new(secret_generator.call(salt, **secret_generator_options), **options)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/class/attribute"
|
4
|
+
require_relative "metadata"
|
5
|
+
require_relative "serializer_with_fallback"
|
6
|
+
|
7
|
+
module ActiveSupport
|
8
|
+
module Messages # :nodoc:
|
9
|
+
class Codec # :nodoc:
|
10
|
+
include Metadata
|
11
|
+
|
12
|
+
class_attribute :default_serializer, default: :marshal,
|
13
|
+
instance_accessor: false, instance_predicate: false
|
14
|
+
|
15
|
+
def initialize(**options)
|
16
|
+
@serializer = options[:serializer] || self.class.default_serializer
|
17
|
+
@serializer = SerializerWithFallback[@serializer] if @serializer.is_a?(Symbol)
|
18
|
+
@url_safe = options[:url_safe]
|
19
|
+
@force_legacy_metadata_serializer = options[:force_legacy_metadata_serializer]
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
attr_reader :serializer
|
24
|
+
|
25
|
+
def encode(data, url_safe: @url_safe)
|
26
|
+
url_safe ? ::Base64.urlsafe_encode64(data, padding: false) : ::Base64.strict_encode64(data)
|
27
|
+
end
|
28
|
+
|
29
|
+
def decode(encoded, url_safe: @url_safe)
|
30
|
+
url_safe ? ::Base64.urlsafe_decode64(encoded) : ::Base64.strict_decode64(encoded)
|
31
|
+
rescue StandardError => error
|
32
|
+
throw :invalid_message_format, error
|
33
|
+
end
|
34
|
+
|
35
|
+
def serialize(data)
|
36
|
+
serializer.dump(data)
|
37
|
+
end
|
38
|
+
|
39
|
+
def deserialize(serialized)
|
40
|
+
serializer.load(serialized)
|
41
|
+
rescue StandardError => error
|
42
|
+
throw :invalid_message_serialization, error
|
43
|
+
end
|
44
|
+
|
45
|
+
def catch_and_ignore(throwable, &block)
|
46
|
+
catch throwable do
|
47
|
+
return block.call
|
48
|
+
end
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def catch_and_raise(throwable, as: nil, &block)
|
53
|
+
error = catch throwable do
|
54
|
+
return block.call
|
55
|
+
end
|
56
|
+
error = as.new(error.to_s) if as
|
57
|
+
raise error
|
58
|
+
end
|
59
|
+
|
60
|
+
def use_message_serializer_for_metadata?
|
61
|
+
!@force_legacy_metadata_serializer && super
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|