activesupport 6.1.0 → 7.1.5.1

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