activesupport 7.0.8.7 → 7.2.3

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