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.
Files changed (171) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +722 -314
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +4 -4
  5. data/lib/active_support/actionable_error.rb +3 -1
  6. data/lib/active_support/array_inquirer.rb +2 -0
  7. data/lib/active_support/backtrace_cleaner.rb +25 -5
  8. data/lib/active_support/benchmarkable.rb +1 -0
  9. data/lib/active_support/builder.rb +1 -1
  10. data/lib/active_support/cache/coder.rb +153 -0
  11. data/lib/active_support/cache/entry.rb +128 -0
  12. data/lib/active_support/cache/file_store.rb +36 -9
  13. data/lib/active_support/cache/mem_cache_store.rb +84 -68
  14. data/lib/active_support/cache/memory_store.rb +76 -24
  15. data/lib/active_support/cache/null_store.rb +6 -0
  16. data/lib/active_support/cache/redis_cache_store.rb +126 -131
  17. data/lib/active_support/cache/serializer_with_fallback.rb +175 -0
  18. data/lib/active_support/cache/strategy/local_cache.rb +20 -8
  19. data/lib/active_support/cache.rb +304 -246
  20. data/lib/active_support/callbacks.rb +38 -18
  21. data/lib/active_support/concern.rb +4 -2
  22. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +42 -3
  23. data/lib/active_support/concurrency/null_lock.rb +13 -0
  24. data/lib/active_support/configurable.rb +10 -0
  25. data/lib/active_support/core_ext/array/conversions.rb +2 -1
  26. data/lib/active_support/core_ext/array.rb +0 -1
  27. data/lib/active_support/core_ext/class/subclasses.rb +13 -10
  28. data/lib/active_support/core_ext/date/conversions.rb +1 -0
  29. data/lib/active_support/core_ext/date.rb +0 -1
  30. data/lib/active_support/core_ext/date_and_time/calculations.rb +10 -0
  31. data/lib/active_support/core_ext/date_time/conversions.rb +6 -2
  32. data/lib/active_support/core_ext/date_time.rb +0 -1
  33. data/lib/active_support/core_ext/digest/uuid.rb +1 -10
  34. data/lib/active_support/core_ext/enumerable.rb +3 -75
  35. data/lib/active_support/core_ext/erb/util.rb +196 -0
  36. data/lib/active_support/core_ext/hash/conversions.rb +1 -1
  37. data/lib/active_support/core_ext/module/attribute_accessors.rb +6 -0
  38. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +34 -16
  39. data/lib/active_support/core_ext/module/delegation.rb +40 -11
  40. data/lib/active_support/core_ext/module/deprecation.rb +15 -12
  41. data/lib/active_support/core_ext/module/introspection.rb +0 -1
  42. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  43. data/lib/active_support/core_ext/numeric/conversions.rb +2 -0
  44. data/lib/active_support/core_ext/numeric.rb +0 -1
  45. data/lib/active_support/core_ext/object/deep_dup.rb +16 -0
  46. data/lib/active_support/core_ext/object/duplicable.rb +15 -24
  47. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  48. data/lib/active_support/core_ext/object/instance_variables.rb +22 -12
  49. data/lib/active_support/core_ext/object/json.rb +10 -2
  50. data/lib/active_support/core_ext/object/with.rb +44 -0
  51. data/lib/active_support/core_ext/object/with_options.rb +3 -3
  52. data/lib/active_support/core_ext/object.rb +1 -0
  53. data/lib/active_support/core_ext/pathname/blank.rb +16 -0
  54. data/lib/active_support/core_ext/pathname/existence.rb +2 -0
  55. data/lib/active_support/core_ext/pathname.rb +1 -0
  56. data/lib/active_support/core_ext/range/conversions.rb +28 -7
  57. data/lib/active_support/core_ext/range/{overlaps.rb → overlap.rb} +5 -3
  58. data/lib/active_support/core_ext/range.rb +1 -2
  59. data/lib/active_support/core_ext/securerandom.rb +24 -12
  60. data/lib/active_support/core_ext/string/filters.rb +20 -14
  61. data/lib/active_support/core_ext/string/inflections.rb +16 -5
  62. data/lib/active_support/core_ext/string/output_safety.rb +38 -174
  63. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  64. data/lib/active_support/core_ext/time/calculations.rb +18 -2
  65. data/lib/active_support/core_ext/time/conversions.rb +2 -2
  66. data/lib/active_support/core_ext/time/zones.rb +4 -4
  67. data/lib/active_support/core_ext/time.rb +0 -1
  68. data/lib/active_support/current_attributes.rb +15 -6
  69. data/lib/active_support/dependencies/autoload.rb +17 -12
  70. data/lib/active_support/deprecation/behaviors.rb +53 -32
  71. data/lib/active_support/deprecation/constant_accessor.rb +5 -4
  72. data/lib/active_support/deprecation/deprecators.rb +104 -0
  73. data/lib/active_support/deprecation/disallowed.rb +3 -5
  74. data/lib/active_support/deprecation/instance_delegator.rb +31 -4
  75. data/lib/active_support/deprecation/method_wrappers.rb +6 -23
  76. data/lib/active_support/deprecation/proxy_wrappers.rb +37 -22
  77. data/lib/active_support/deprecation/reporting.rb +35 -21
  78. data/lib/active_support/deprecation.rb +32 -5
  79. data/lib/active_support/deprecator.rb +7 -0
  80. data/lib/active_support/descendants_tracker.rb +104 -132
  81. data/lib/active_support/duration/iso8601_serializer.rb +0 -2
  82. data/lib/active_support/duration.rb +2 -1
  83. data/lib/active_support/encrypted_configuration.rb +30 -9
  84. data/lib/active_support/encrypted_file.rb +8 -3
  85. data/lib/active_support/environment_inquirer.rb +22 -2
  86. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  87. data/lib/active_support/error_reporter.rb +121 -35
  88. data/lib/active_support/execution_wrapper.rb +4 -4
  89. data/lib/active_support/file_update_checker.rb +4 -2
  90. data/lib/active_support/fork_tracker.rb +10 -2
  91. data/lib/active_support/gem_version.rb +4 -4
  92. data/lib/active_support/gzip.rb +2 -0
  93. data/lib/active_support/hash_with_indifferent_access.rb +35 -17
  94. data/lib/active_support/i18n.rb +1 -1
  95. data/lib/active_support/i18n_railtie.rb +20 -13
  96. data/lib/active_support/inflector/inflections.rb +2 -0
  97. data/lib/active_support/inflector/methods.rb +22 -10
  98. data/lib/active_support/inflector/transliterate.rb +3 -1
  99. data/lib/active_support/isolated_execution_state.rb +26 -22
  100. data/lib/active_support/json/decoding.rb +2 -1
  101. data/lib/active_support/json/encoding.rb +25 -43
  102. data/lib/active_support/key_generator.rb +9 -1
  103. data/lib/active_support/lazy_load_hooks.rb +6 -4
  104. data/lib/active_support/locale/en.yml +2 -0
  105. data/lib/active_support/log_subscriber.rb +78 -33
  106. data/lib/active_support/logger.rb +1 -1
  107. data/lib/active_support/logger_thread_safe_level.rb +9 -21
  108. data/lib/active_support/message_encryptor.rb +197 -53
  109. data/lib/active_support/message_encryptors.rb +140 -0
  110. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  111. data/lib/active_support/message_pack/extensions.rb +292 -0
  112. data/lib/active_support/message_pack/serializer.rb +63 -0
  113. data/lib/active_support/message_pack.rb +50 -0
  114. data/lib/active_support/message_verifier.rb +212 -93
  115. data/lib/active_support/message_verifiers.rb +134 -0
  116. data/lib/active_support/messages/codec.rb +65 -0
  117. data/lib/active_support/messages/metadata.rb +111 -45
  118. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  119. data/lib/active_support/messages/rotator.rb +34 -32
  120. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  121. data/lib/active_support/multibyte/chars.rb +2 -0
  122. data/lib/active_support/multibyte/unicode.rb +9 -37
  123. data/lib/active_support/notifications/fanout.rb +239 -81
  124. data/lib/active_support/notifications/instrumenter.rb +71 -14
  125. data/lib/active_support/notifications.rb +1 -1
  126. data/lib/active_support/number_helper/number_converter.rb +2 -2
  127. data/lib/active_support/number_helper/number_to_human_size_converter.rb +1 -1
  128. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -0
  129. data/lib/active_support/ordered_hash.rb +3 -3
  130. data/lib/active_support/ordered_options.rb +14 -0
  131. data/lib/active_support/parameter_filter.rb +84 -69
  132. data/lib/active_support/proxy_object.rb +2 -0
  133. data/lib/active_support/railtie.rb +33 -21
  134. data/lib/active_support/reloader.rb +12 -4
  135. data/lib/active_support/rescuable.rb +2 -0
  136. data/lib/active_support/secure_compare_rotator.rb +16 -9
  137. data/lib/active_support/string_inquirer.rb +3 -1
  138. data/lib/active_support/subscriber.rb +9 -27
  139. data/lib/active_support/syntax_error_proxy.rb +49 -0
  140. data/lib/active_support/tagged_logging.rb +60 -24
  141. data/lib/active_support/test_case.rb +153 -6
  142. data/lib/active_support/testing/assertions.rb +25 -9
  143. data/lib/active_support/testing/autorun.rb +0 -2
  144. data/lib/active_support/testing/constant_stubbing.rb +32 -0
  145. data/lib/active_support/testing/deprecation.rb +25 -25
  146. data/lib/active_support/testing/error_reporter_assertions.rb +108 -0
  147. data/lib/active_support/testing/isolation.rb +1 -1
  148. data/lib/active_support/testing/method_call_assertions.rb +21 -8
  149. data/lib/active_support/testing/parallelize_executor.rb +8 -3
  150. data/lib/active_support/testing/stream.rb +1 -1
  151. data/lib/active_support/testing/strict_warnings.rb +38 -0
  152. data/lib/active_support/testing/time_helpers.rb +32 -14
  153. data/lib/active_support/time_with_zone.rb +4 -14
  154. data/lib/active_support/values/time_zone.rb +9 -7
  155. data/lib/active_support/version.rb +1 -1
  156. data/lib/active_support/xml_mini/jdom.rb +3 -10
  157. data/lib/active_support/xml_mini/nokogiri.rb +1 -1
  158. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  159. data/lib/active_support/xml_mini/rexml.rb +1 -1
  160. data/lib/active_support/xml_mini.rb +2 -2
  161. data/lib/active_support.rb +13 -3
  162. metadata +106 -21
  163. data/lib/active_support/core_ext/array/deprecated_conversions.rb +0 -25
  164. data/lib/active_support/core_ext/date/deprecated_conversions.rb +0 -40
  165. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +0 -36
  166. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +0 -60
  167. data/lib/active_support/core_ext/range/deprecated_conversions.rb +0 -36
  168. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -5
  169. data/lib/active_support/core_ext/time/deprecated_conversions.rb +0 -73
  170. data/lib/active_support/core_ext/uri.rb +0 -5
  171. 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/metadata"
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
- # Remember Me:
18
- # cookies[:remember_me] = @verifier.generate([@user.id, 2.weeks.from_now])
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
- # In the authentication filter:
26
+ # Later verify that message:
21
27
  #
22
- # id, time = @verifier.verify(cookies[:remember_me])
23
- # if Time.now < time
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
- # By default it uses Marshal to serialize the message. If you want to use
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
- # @verifier = ActiveSupport::MessageVerifier.new('s3Krit', digest: 'SHA256')
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
- # === Confining messages to a specific purpose
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) # => "this is the chair"
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) # => "this is the chair"
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("the conversation is lively")
60
- # @verifier.verified(token, purpose: :scare_tactics) # => nil
61
- # @verifier.verified(token) # => "the conversation is lively"
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: :scare_tactics) # => ActiveSupport::MessageVerifier::InvalidSignature
64
- # @verifier.verify(token) # => "the conversation is lively"
59
+ # @verifier.verify(token, purpose: :redirect) # => raises ActiveSupport::MessageVerifier::InvalidSignature
60
+ # @verifier.verify(token) # => "signed message"
65
61
  #
66
- # === Making messages expire
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("parcel", expires_in: 1.month)
73
- # @verifier.generate("doowad", expires_at: Time.now.end_of_year)
68
+ # @verifier.generate("signed message", expires_in: 1.month)
69
+ # @verifier.generate("signed message", expires_at: Time.now.end_of_year)
74
70
  #
75
- # Then the messages can be verified and returned up to the expire time.
71
+ # Messages can then be verified and returned until expiry.
76
72
  # Thereafter, the +verified+ method returns +nil+ while +verify+ raises
77
- # <tt>ActiveSupport::MessageVerifier::InvalidSignature</tt>.
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 old_secret # Fallback to an old secret instead of @secret.
96
- # verifier.rotate digest: "SHA256" # Fallback to an old digest instead of SHA512.
97
- # verifier.rotate serializer: Marshal # Fallback to an old serializer instead of JSON.
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 old_secret, digest: "SHA256", serializer: Marshal
102
- class MessageVerifier
103
- prepend Messages::Rotator::Verifier
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
- def initialize(secret, digest: nil, serializer: nil)
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 's3Krit'
121
- # signed_message = verifier.generate 'a private message'
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?(signed_message)
127
- data, digest = get_data_and_digest_from(signed_message)
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 's3Krit'
175
+ # verifier = ActiveSupport::MessageVerifier.new("secret")
134
176
  #
135
- # signed_message = verifier.generate 'a private message'
136
- # verifier.verified(signed_message) # => 'a private 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 'd1ff3r3nt-s3Krit'
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
- def verified(signed_message, purpose: nil, **)
153
- data, digest = get_data_and_digest_from(signed_message)
154
- if digest_matches_data?(digest, data)
155
- begin
156
- message = Messages::Metadata.verify(decode(data), purpose)
157
- @serializer.load(message) if message
158
- rescue ArgumentError => argument_error
159
- return if argument_error.message.include?("invalid base64")
160
- raise
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 's3Krit'
168
- # signed_message = verifier.generate 'a private message'
222
+ # verifier = ActiveSupport::MessageVerifier.new("secret")
223
+ # signed_message = verifier.generate("signed message")
169
224
  #
170
- # verifier.verify(signed_message) # => 'a private 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 'd1ff3r3nt-s3Krit'
230
+ # other_verifier = ActiveSupport::MessageVerifier.new("different_secret")
176
231
  # other_verifier.verify(signed_message) # => ActiveSupport::MessageVerifier::InvalidSignature
177
- def verify(*args, **options)
178
- verified(*args, **options) || raise(InvalidSignature)
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 's3Krit'
187
- # verifier.generate 'a private message' # => "BAhJIhRwcml2YXRlLW1lc3NhZ2UGOgZFVA==--e2d724331ebdee96a10fb99b089508d1c72bd772"
188
- def generate(value, expires_at: nil, expires_in: nil, purpose: nil)
189
- data = encode(Messages::Metadata.wrap(@serializer.dump(value), expires_at: expires_at, expires_in: expires_in, purpose: purpose))
190
- "#{data}#{SEPARATOR}#{generate_digest(data)}"
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 encode(data)
195
- ::Base64.strict_encode64(data)
309
+ def sign_encoded(encoded)
310
+ digest = generate_digest(encoded)
311
+ encoded << SEPARATOR << digest
196
312
  end
197
313
 
198
- def decode(data)
199
- ::Base64.strict_decode64(data)
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 separator_index_for(signed_message)
215
- index = signed_message.length - digest_length_in_hex - SEPARATOR_LENGTH
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 get_data_and_digest_from(signed_message)
222
- return if signed_message.nil? || !signed_message.valid_encoding? || signed_message.empty?
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