activesupport 7.0.8.7 → 7.1.0.beta1
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 +722 -314
- data/MIT-LICENSE +1 -1
- data/README.rdoc +4 -4
- data/lib/active_support/actionable_error.rb +3 -1
- data/lib/active_support/array_inquirer.rb +2 -0
- data/lib/active_support/backtrace_cleaner.rb +25 -5
- data/lib/active_support/benchmarkable.rb +1 -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 +128 -0
- data/lib/active_support/cache/file_store.rb +36 -9
- data/lib/active_support/cache/mem_cache_store.rb +84 -68
- data/lib/active_support/cache/memory_store.rb +76 -24
- data/lib/active_support/cache/null_store.rb +6 -0
- data/lib/active_support/cache/redis_cache_store.rb +126 -131
- data/lib/active_support/cache/serializer_with_fallback.rb +175 -0
- data/lib/active_support/cache/strategy/local_cache.rb +20 -8
- data/lib/active_support/cache.rb +304 -246
- data/lib/active_support/callbacks.rb +38 -18
- data/lib/active_support/concern.rb +4 -2
- data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +42 -3
- data/lib/active_support/concurrency/null_lock.rb +13 -0
- data/lib/active_support/configurable.rb +10 -0
- data/lib/active_support/core_ext/array/conversions.rb +2 -1
- data/lib/active_support/core_ext/array.rb +0 -1
- data/lib/active_support/core_ext/class/subclasses.rb +13 -10
- data/lib/active_support/core_ext/date/conversions.rb +1 -0
- data/lib/active_support/core_ext/date.rb +0 -1
- data/lib/active_support/core_ext/date_and_time/calculations.rb +10 -0
- data/lib/active_support/core_ext/date_time/conversions.rb +6 -2
- data/lib/active_support/core_ext/date_time.rb +0 -1
- data/lib/active_support/core_ext/digest/uuid.rb +1 -10
- data/lib/active_support/core_ext/enumerable.rb +3 -75
- data/lib/active_support/core_ext/erb/util.rb +196 -0
- data/lib/active_support/core_ext/hash/conversions.rb +1 -1
- data/lib/active_support/core_ext/module/attribute_accessors.rb +6 -0
- data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +34 -16
- data/lib/active_support/core_ext/module/delegation.rb +40 -11
- 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/numeric/bytes.rb +9 -0
- data/lib/active_support/core_ext/numeric/conversions.rb +2 -0
- data/lib/active_support/core_ext/numeric.rb +0 -1
- data/lib/active_support/core_ext/object/deep_dup.rb +16 -0
- data/lib/active_support/core_ext/object/duplicable.rb +15 -24
- 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 +10 -2
- data/lib/active_support/core_ext/object/with.rb +44 -0
- data/lib/active_support/core_ext/object/with_options.rb +3 -3
- 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 +2 -0
- data/lib/active_support/core_ext/pathname.rb +1 -0
- data/lib/active_support/core_ext/range/conversions.rb +28 -7
- data/lib/active_support/core_ext/range/{overlaps.rb → overlap.rb} +5 -3
- data/lib/active_support/core_ext/range.rb +1 -2
- data/lib/active_support/core_ext/securerandom.rb +24 -12
- data/lib/active_support/core_ext/string/filters.rb +20 -14
- data/lib/active_support/core_ext/string/inflections.rb +16 -5
- data/lib/active_support/core_ext/string/output_safety.rb +38 -174
- data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
- data/lib/active_support/core_ext/time/calculations.rb +18 -2
- data/lib/active_support/core_ext/time/conversions.rb +2 -2
- data/lib/active_support/core_ext/time/zones.rb +4 -4
- data/lib/active_support/core_ext/time.rb +0 -1
- data/lib/active_support/current_attributes.rb +15 -6
- data/lib/active_support/dependencies/autoload.rb +17 -12
- data/lib/active_support/deprecation/behaviors.rb +53 -32
- 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 +3 -5
- data/lib/active_support/deprecation/instance_delegator.rb +31 -4
- data/lib/active_support/deprecation/method_wrappers.rb +6 -23
- data/lib/active_support/deprecation/proxy_wrappers.rb +37 -22
- data/lib/active_support/deprecation/reporting.rb +35 -21
- data/lib/active_support/deprecation.rb +32 -5
- data/lib/active_support/deprecator.rb +7 -0
- data/lib/active_support/descendants_tracker.rb +104 -132
- data/lib/active_support/duration/iso8601_serializer.rb +0 -2
- data/lib/active_support/duration.rb +2 -1
- data/lib/active_support/encrypted_configuration.rb +30 -9
- data/lib/active_support/encrypted_file.rb +8 -3
- data/lib/active_support/environment_inquirer.rb +22 -2
- data/lib/active_support/error_reporter/test_helper.rb +15 -0
- data/lib/active_support/error_reporter.rb +121 -35
- data/lib/active_support/execution_wrapper.rb +4 -4
- data/lib/active_support/file_update_checker.rb +4 -2
- data/lib/active_support/fork_tracker.rb +10 -2
- 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 +35 -17
- data/lib/active_support/i18n.rb +1 -1
- data/lib/active_support/i18n_railtie.rb +20 -13
- data/lib/active_support/inflector/inflections.rb +2 -0
- data/lib/active_support/inflector/methods.rb +22 -10
- data/lib/active_support/inflector/transliterate.rb +3 -1
- data/lib/active_support/isolated_execution_state.rb +26 -22
- data/lib/active_support/json/decoding.rb +2 -1
- data/lib/active_support/json/encoding.rb +25 -43
- data/lib/active_support/key_generator.rb +9 -1
- data/lib/active_support/lazy_load_hooks.rb +6 -4
- data/lib/active_support/locale/en.yml +2 -0
- data/lib/active_support/log_subscriber.rb +78 -33
- data/lib/active_support/logger.rb +1 -1
- data/lib/active_support/logger_thread_safe_level.rb +9 -21
- data/lib/active_support/message_encryptor.rb +197 -53
- data/lib/active_support/message_encryptors.rb +140 -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 +212 -93
- data/lib/active_support/message_verifiers.rb +134 -0
- data/lib/active_support/messages/codec.rb +65 -0
- data/lib/active_support/messages/metadata.rb +111 -45
- 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 +2 -0
- data/lib/active_support/multibyte/unicode.rb +9 -37
- data/lib/active_support/notifications/fanout.rb +239 -81
- data/lib/active_support/notifications/instrumenter.rb +71 -14
- data/lib/active_support/notifications.rb +1 -1
- data/lib/active_support/number_helper/number_converter.rb +2 -2
- data/lib/active_support/number_helper/number_to_human_size_converter.rb +1 -1
- data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -0
- data/lib/active_support/ordered_hash.rb +3 -3
- data/lib/active_support/ordered_options.rb +14 -0
- data/lib/active_support/parameter_filter.rb +84 -69
- data/lib/active_support/proxy_object.rb +2 -0
- data/lib/active_support/railtie.rb +33 -21
- data/lib/active_support/reloader.rb +12 -4
- data/lib/active_support/rescuable.rb +2 -0
- data/lib/active_support/secure_compare_rotator.rb +16 -9
- data/lib/active_support/string_inquirer.rb +3 -1
- data/lib/active_support/subscriber.rb +9 -27
- data/lib/active_support/syntax_error_proxy.rb +49 -0
- data/lib/active_support/tagged_logging.rb +60 -24
- data/lib/active_support/test_case.rb +153 -6
- data/lib/active_support/testing/assertions.rb +25 -9
- 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 +25 -25
- data/lib/active_support/testing/error_reporter_assertions.rb +108 -0
- data/lib/active_support/testing/isolation.rb +1 -1
- data/lib/active_support/testing/method_call_assertions.rb +21 -8
- data/lib/active_support/testing/parallelize_executor.rb +8 -3
- data/lib/active_support/testing/stream.rb +1 -1
- data/lib/active_support/testing/strict_warnings.rb +38 -0
- data/lib/active_support/testing/time_helpers.rb +32 -14
- data/lib/active_support/time_with_zone.rb +4 -14
- data/lib/active_support/values/time_zone.rb +9 -7
- data/lib/active_support/version.rb +1 -1
- data/lib/active_support/xml_mini/jdom.rb +3 -10
- data/lib/active_support/xml_mini/nokogiri.rb +1 -1
- data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
- data/lib/active_support/xml_mini/rexml.rb +1 -1
- data/lib/active_support/xml_mini.rb +2 -2
- data/lib/active_support.rb +13 -3
- metadata +106 -21
- data/lib/active_support/core_ext/array/deprecated_conversions.rb +0 -25
- data/lib/active_support/core_ext/date/deprecated_conversions.rb +0 -40
- data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +0 -36
- data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +0 -60
- data/lib/active_support/core_ext/range/deprecated_conversions.rb +0 -36
- data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -5
- data/lib/active_support/core_ext/time/deprecated_conversions.rb +0 -73
- data/lib/active_support/core_ext/uri.rb +0 -5
- data/lib/active_support/per_thread_registry.rb +0 -65
@@ -4,77 +4,73 @@ require "openssl"
|
|
4
4
|
require "base64"
|
5
5
|
require "active_support/core_ext/object/blank"
|
6
6
|
require "active_support/security_utils"
|
7
|
-
require "active_support/messages/
|
7
|
+
require "active_support/messages/codec"
|
8
8
|
require "active_support/messages/rotator"
|
9
9
|
|
10
10
|
module ActiveSupport
|
11
|
+
# = Active Support Message Verifier
|
12
|
+
#
|
11
13
|
# +MessageVerifier+ makes it easy to generate and verify messages which are
|
12
14
|
# signed to prevent tampering.
|
13
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
|
+
#
|
14
20
|
# This is useful for cases like remember-me tokens and auto-unsubscribe links
|
15
21
|
# where the session store isn't suitable or available.
|
16
22
|
#
|
17
|
-
#
|
18
|
-
# 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])
|
19
25
|
#
|
20
|
-
#
|
26
|
+
# Later verify that message:
|
21
27
|
#
|
22
|
-
# id, time =
|
23
|
-
# if
|
28
|
+
# id, time = Rails.application.message_verifier(:remember_me).verify(cookies[:remember_me])
|
29
|
+
# if time.future?
|
24
30
|
# self.current_user = User.find(id)
|
25
31
|
# end
|
26
32
|
#
|
27
|
-
#
|
28
|
-
# another serialization method, you can set the serializer in the options
|
29
|
-
# hash upon initialization:
|
30
|
-
#
|
31
|
-
# @verifier = ActiveSupport::MessageVerifier.new('s3Krit', serializer: YAML)
|
32
|
-
#
|
33
|
-
# +MessageVerifier+ creates HMAC signatures using SHA1 hash algorithm by default.
|
34
|
-
# If you want to use a different hash algorithm, you can change it by providing
|
35
|
-
# +:digest+ key as an option while initializing the verifier:
|
33
|
+
# === Confine messages to a specific purpose
|
36
34
|
#
|
37
|
-
#
|
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+.
|
38
39
|
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
# By default any message can be used throughout your app. But they can also be
|
42
|
-
# confined to a specific +:purpose+.
|
43
|
-
#
|
44
|
-
# token = @verifier.generate("this is the chair", purpose: :login)
|
40
|
+
# token = @verifier.generate("signed message", purpose: :login)
|
45
41
|
#
|
46
42
|
# Then that same purpose must be passed when verifying to get the data back out:
|
47
43
|
#
|
48
|
-
# @verifier.verified(token, purpose: :login) # => "
|
44
|
+
# @verifier.verified(token, purpose: :login) # => "signed message"
|
49
45
|
# @verifier.verified(token, purpose: :shipping) # => nil
|
50
46
|
# @verifier.verified(token) # => nil
|
51
47
|
#
|
52
|
-
# @verifier.verify(token, purpose: :login) # => "
|
53
|
-
# @verifier.verify(token, purpose: :shipping) # => ActiveSupport::MessageVerifier::InvalidSignature
|
54
|
-
# @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
|
55
51
|
#
|
56
52
|
# Likewise, if a message has no purpose it won't be returned when verifying with
|
57
53
|
# a specific purpose.
|
58
54
|
#
|
59
|
-
# token = @verifier.generate("
|
60
|
-
# @verifier.verified(token, purpose: :
|
61
|
-
# @verifier.verified(token)
|
55
|
+
# token = @verifier.generate("signed message")
|
56
|
+
# @verifier.verified(token, purpose: :redirect) # => nil
|
57
|
+
# @verifier.verified(token) # => "signed message"
|
62
58
|
#
|
63
|
-
# @verifier.verify(token, purpose: :
|
64
|
-
# @verifier.verify(token)
|
59
|
+
# @verifier.verify(token, purpose: :redirect) # => raises ActiveSupport::MessageVerifier::InvalidSignature
|
60
|
+
# @verifier.verify(token) # => "signed message"
|
65
61
|
#
|
66
|
-
# ===
|
62
|
+
# === Expiring messages
|
67
63
|
#
|
68
64
|
# By default messages last forever and verifying one year from now will still
|
69
65
|
# return the original value. But messages can be set to expire at a given
|
70
66
|
# time with +:expires_in+ or +:expires_at+.
|
71
67
|
#
|
72
|
-
# @verifier.generate("
|
73
|
-
# @verifier.generate("
|
68
|
+
# @verifier.generate("signed message", expires_in: 1.month)
|
69
|
+
# @verifier.generate("signed message", expires_at: Time.now.end_of_year)
|
74
70
|
#
|
75
|
-
#
|
71
|
+
# Messages can then be verified and returned until expiry.
|
76
72
|
# Thereafter, the +verified+ method returns +nil+ while +verify+ raises
|
77
|
-
#
|
73
|
+
# +ActiveSupport::MessageVerifier::InvalidSignature+.
|
78
74
|
#
|
79
75
|
# === Rotating keys
|
80
76
|
#
|
@@ -92,52 +88,98 @@ module ActiveSupport
|
|
92
88
|
# Then gradually rotate the old values out by adding them as fallbacks. Any message
|
93
89
|
# generated with the old values will then work until the rotation is removed.
|
94
90
|
#
|
95
|
-
# verifier.rotate
|
96
|
-
# verifier.rotate
|
97
|
-
# 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.
|
98
94
|
#
|
99
95
|
# Though the above would most likely be combined into one rotation:
|
100
96
|
#
|
101
|
-
# verifier.rotate
|
102
|
-
class MessageVerifier
|
103
|
-
prepend Messages::Rotator
|
97
|
+
# verifier.rotate(old_secret, digest: "SHA256", serializer: Marshal)
|
98
|
+
class MessageVerifier < Messages::Codec
|
99
|
+
prepend Messages::Rotator
|
104
100
|
|
105
101
|
class InvalidSignature < StandardError; end
|
106
102
|
|
107
103
|
SEPARATOR = "--" # :nodoc:
|
108
104
|
SEPARATOR_LENGTH = SEPARATOR.length # :nodoc:
|
109
105
|
|
110
|
-
|
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)
|
111
154
|
raise ArgumentError, "Secret should not be nil." unless secret
|
155
|
+
super(**options)
|
112
156
|
@secret = secret
|
113
|
-
@digest = digest&.to_s || "SHA1"
|
114
|
-
@serializer = serializer || Marshal
|
157
|
+
@digest = options[:digest]&.to_s || "SHA1"
|
115
158
|
end
|
116
159
|
|
117
160
|
# Checks if a signed message could have been generated by signing an object
|
118
161
|
# with the +MessageVerifier+'s secret.
|
119
162
|
#
|
120
|
-
# verifier = ActiveSupport::MessageVerifier.new
|
121
|
-
# signed_message = verifier.generate
|
163
|
+
# verifier = ActiveSupport::MessageVerifier.new("secret")
|
164
|
+
# signed_message = verifier.generate("signed message")
|
122
165
|
# verifier.valid_message?(signed_message) # => true
|
123
166
|
#
|
124
167
|
# tampered_message = signed_message.chop # editing the message invalidates the signature
|
125
168
|
# verifier.valid_message?(tampered_message) # => false
|
126
|
-
def valid_message?(
|
127
|
-
|
128
|
-
digest_matches_data?(digest, data)
|
169
|
+
def valid_message?(message)
|
170
|
+
!!catch_and_ignore(:invalid_message_format) { extract_encoded(message) }
|
129
171
|
end
|
130
172
|
|
131
173
|
# Decodes the signed message using the +MessageVerifier+'s secret.
|
132
174
|
#
|
133
|
-
# verifier = ActiveSupport::MessageVerifier.new
|
175
|
+
# verifier = ActiveSupport::MessageVerifier.new("secret")
|
134
176
|
#
|
135
|
-
# signed_message = verifier.generate
|
136
|
-
# verifier.verified(signed_message) # =>
|
177
|
+
# signed_message = verifier.generate("signed message")
|
178
|
+
# verifier.verified(signed_message) # => "signed message"
|
137
179
|
#
|
138
180
|
# Returns +nil+ if the message was not signed with the same secret.
|
139
181
|
#
|
140
|
-
# other_verifier = ActiveSupport::MessageVerifier.new
|
182
|
+
# other_verifier = ActiveSupport::MessageVerifier.new("different_secret")
|
141
183
|
# other_verifier.verified(signed_message) # => nil
|
142
184
|
#
|
143
185
|
# Returns +nil+ if the message is not Base64-encoded.
|
@@ -149,33 +191,68 @@ module ActiveSupport
|
|
149
191
|
#
|
150
192
|
# incompatible_message = "test--dad7b06c94abba8d46a15fafaef56c327665d5ff"
|
151
193
|
# verifier.verified(incompatible_message) # => TypeError: incompatible marshal file format
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
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
|
161
216
|
end
|
162
217
|
end
|
163
218
|
end
|
164
219
|
|
165
220
|
# Decodes the signed message using the +MessageVerifier+'s secret.
|
166
221
|
#
|
167
|
-
# verifier = ActiveSupport::MessageVerifier.new
|
168
|
-
# signed_message = verifier.generate
|
222
|
+
# verifier = ActiveSupport::MessageVerifier.new("secret")
|
223
|
+
# signed_message = verifier.generate("signed message")
|
169
224
|
#
|
170
|
-
# verifier.verify(signed_message) # =>
|
225
|
+
# verifier.verify(signed_message) # => "signed message"
|
171
226
|
#
|
172
227
|
# Raises +InvalidSignature+ if the message was not signed with the same
|
173
228
|
# secret or was not Base64-encoded.
|
174
229
|
#
|
175
|
-
# other_verifier = ActiveSupport::MessageVerifier.new
|
230
|
+
# other_verifier = ActiveSupport::MessageVerifier.new("different_secret")
|
176
231
|
# other_verifier.verify(signed_message) # => ActiveSupport::MessageVerifier::InvalidSignature
|
177
|
-
|
178
|
-
|
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
|
179
256
|
end
|
180
257
|
|
181
258
|
# Generates a signed message for the provided value.
|
@@ -183,20 +260,72 @@ module ActiveSupport
|
|
183
260
|
# The message is signed with the +MessageVerifier+'s secret.
|
184
261
|
# Returns Base64-encoded message joined with the generated signature.
|
185
262
|
#
|
186
|
-
# verifier = ActiveSupport::MessageVerifier.new
|
187
|
-
# verifier.generate
|
188
|
-
|
189
|
-
|
190
|
-
|
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)}>"
|
191
306
|
end
|
192
307
|
|
193
308
|
private
|
194
|
-
def
|
195
|
-
|
309
|
+
def sign_encoded(encoded)
|
310
|
+
digest = generate_digest(encoded)
|
311
|
+
encoded << SEPARATOR << digest
|
196
312
|
end
|
197
313
|
|
198
|
-
def
|
199
|
-
|
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
|
200
329
|
end
|
201
330
|
|
202
331
|
def generate_digest(data)
|
@@ -211,23 +340,13 @@ module ActiveSupport
|
|
211
340
|
@digest_length_in_hex ||= OpenSSL::Digest.new(@digest).digest_length * 2
|
212
341
|
end
|
213
342
|
|
214
|
-
def
|
215
|
-
index
|
216
|
-
return if index.negative? || signed_message[index, SEPARATOR_LENGTH] != SEPARATOR
|
217
|
-
|
218
|
-
index
|
343
|
+
def separator_at?(signed_message, index)
|
344
|
+
signed_message[index, SEPARATOR_LENGTH] == SEPARATOR
|
219
345
|
end
|
220
346
|
|
221
|
-
def
|
222
|
-
|
223
|
-
|
224
|
-
separator_index = separator_index_for(signed_message)
|
225
|
-
return if separator_index.nil?
|
226
|
-
|
227
|
-
data = signed_message[0...separator_index]
|
228
|
-
digest = signed_message[separator_index + SEPARATOR_LENGTH..-1]
|
229
|
-
|
230
|
-
[data, digest]
|
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)
|
231
350
|
end
|
232
351
|
|
233
352
|
def digest_matches_data?(digest, data)
|
@@ -0,0 +1,134 @@
|
|
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
|
+
private
|
130
|
+
def build(salt, secret_generator:, secret_generator_options:, **options)
|
131
|
+
MessageVerifier.new(secret_generator.call(salt, **secret_generator_options), **options)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
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 ArgumentError => 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
|