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,18 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/concern"
4
- require "active_support/core_ext/module/attribute_accessors"
5
- require "concurrent"
6
- require "fiber"
4
+ require "logger"
7
5
 
8
6
  module ActiveSupport
9
7
  module LoggerThreadSafeLevel # :nodoc:
10
8
  extend ActiveSupport::Concern
11
9
 
12
- included do
13
- cattr_accessor :local_levels, default: Concurrent::Map.new(initial_capacity: 2), instance_accessor: false
14
- end
15
-
16
10
  Logger::Severity.constants.each do |severity|
17
11
  class_eval(<<-EOT, __FILE__, __LINE__ + 1)
18
12
  def #{severity.downcase}? # def debug?
@@ -21,25 +15,24 @@ module ActiveSupport
21
15
  EOT
22
16
  end
23
17
 
24
- def local_log_id
25
- Fiber.current.__id__
26
- end
27
-
28
18
  def local_level
29
- self.class.local_levels[local_log_id]
19
+ IsolatedExecutionState[local_level_key]
30
20
  end
31
21
 
32
22
  def local_level=(level)
33
23
  case level
34
24
  when Integer
35
- self.class.local_levels[local_log_id] = level
36
25
  when Symbol
37
- self.class.local_levels[local_log_id] = Logger::Severity.const_get(level.to_s.upcase)
26
+ level = Logger::Severity.const_get(level.to_s.upcase)
38
27
  when nil
39
- self.class.local_levels.delete(local_log_id)
40
28
  else
41
29
  raise ArgumentError, "Invalid log level: #{level.inspect}"
42
30
  end
31
+ if level.nil?
32
+ IsolatedExecutionState.delete(local_level_key)
33
+ else
34
+ IsolatedExecutionState[local_level_key] = level
35
+ end
43
36
  end
44
37
 
45
38
  def level
@@ -54,25 +47,9 @@ module ActiveSupport
54
47
  self.local_level = old_local_level
55
48
  end
56
49
 
57
- # Redefined to check severity against #level, and thus the thread-local level, rather than +@level+.
58
- # FIXME: Remove when the minimum Ruby version supports overriding Logger#level.
59
- def add(severity, message = nil, progname = nil, &block) #:nodoc:
60
- severity ||= UNKNOWN
61
- progname ||= @progname
62
-
63
- return true if @logdev.nil? || severity < level
64
-
65
- if message.nil?
66
- if block_given?
67
- message = yield
68
- else
69
- message = progname
70
- progname = @progname
71
- end
50
+ private
51
+ def local_level_key
52
+ @local_level_key ||= :"logger_thread_safe_level_#{object_id}"
72
53
  end
73
-
74
- @logdev.write \
75
- format_message(format_severity(severity), Time.now, progname, message)
76
- end
77
54
  end
78
55
  end
@@ -3,17 +3,20 @@
3
3
  require "openssl"
4
4
  require "base64"
5
5
  require "active_support/core_ext/module/attribute_accessors"
6
+ require "active_support/messages/codec"
7
+ require "active_support/messages/rotator"
6
8
  require "active_support/message_verifier"
7
- require "active_support/messages/metadata"
8
9
 
9
10
  module ActiveSupport
11
+ # = Active Support Message Encryptor
12
+ #
10
13
  # MessageEncryptor is a simple way to encrypt values which get stored
11
14
  # somewhere you don't trust.
12
15
  #
13
16
  # The cipher text and initialization vector are base64 encoded and returned
14
17
  # to you.
15
18
  #
16
- # This can be used in situations similar to the <tt>MessageVerifier</tt>, but
19
+ # This can be used in situations similar to the MessageVerifier, but
17
20
  # where you don't want users to be able to determine the value of the payload.
18
21
  #
19
22
  # len = ActiveSupport::MessageEncryptor.key_len
@@ -23,6 +26,12 @@ module ActiveSupport
23
26
  # encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..."
24
27
  # crypt.decrypt_and_verify(encrypted_data) # => "my secret data"
25
28
  #
29
+ # The +decrypt_and_verify+ method will raise an
30
+ # +ActiveSupport::MessageEncryptor::InvalidMessage+ exception if the data
31
+ # provided cannot be decrypted or verified.
32
+ #
33
+ # crypt.decrypt_and_verify('not encrypted data') # => ActiveSupport::MessageEncryptor::InvalidMessage
34
+ #
26
35
  # === Confining messages to a specific purpose
27
36
  #
28
37
  # By default any message can be used throughout your app. But they can also be
@@ -78,13 +87,13 @@ module ActiveSupport
78
87
  # the above should be combined into:
79
88
  #
80
89
  # crypt.rotate old_secret, cipher: "aes-256-cbc"
81
- class MessageEncryptor
82
- prepend Messages::Rotator::Encryptor
90
+ class MessageEncryptor < Messages::Codec
91
+ prepend Messages::Rotator
83
92
 
84
93
  cattr_accessor :use_authenticated_message_encryption, instance_accessor: false, default: false
85
94
 
86
95
  class << self
87
- def default_cipher #:nodoc:
96
+ def default_cipher # :nodoc:
88
97
  if use_authenticated_message_encryption
89
98
  "aes-256-gcm"
90
99
  else
@@ -93,7 +102,7 @@ module ActiveSupport
93
102
  end
94
103
  end
95
104
 
96
- module NullSerializer #:nodoc:
105
+ module NullSerializer # :nodoc:
97
106
  def self.load(value)
98
107
  value
99
108
  end
@@ -103,55 +112,140 @@ module ActiveSupport
103
112
  end
104
113
  end
105
114
 
106
- module NullVerifier #:nodoc:
107
- def self.verify(value)
108
- value
109
- end
110
-
111
- def self.generate(value)
112
- value
113
- end
114
- end
115
-
116
115
  class InvalidMessage < StandardError; end
117
116
  OpenSSLCipherError = OpenSSL::Cipher::CipherError
118
117
 
118
+ AUTH_TAG_LENGTH = 16 # :nodoc:
119
+ SEPARATOR = "--" # :nodoc:
120
+
119
121
  # Initialize a new MessageEncryptor. +secret+ must be at least as long as
120
122
  # the cipher key size. For the default 'aes-256-gcm' cipher, this is 256
121
123
  # bits. If you are using a user-entered secret, you can generate a suitable
122
- # key by using <tt>ActiveSupport::KeyGenerator</tt> or a similar key
124
+ # key by using ActiveSupport::KeyGenerator or a similar key
123
125
  # derivation function.
124
126
  #
125
- # First additional parameter is used as the signature key for +MessageVerifier+.
126
- # This allows you to specify keys to encrypt and sign data.
127
+ # The first additional parameter is used as the signature key for
128
+ # MessageVerifier. This allows you to specify keys to encrypt and sign
129
+ # data. Ignored when using an AEAD cipher like 'aes-256-gcm'.
127
130
  #
128
131
  # ActiveSupport::MessageEncryptor.new('secret', 'signature_secret')
129
132
  #
130
- # Options:
131
- # * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by
132
- # <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-gcm'.
133
- # * <tt>:digest</tt> - String of digest to use for signing. Default is
134
- # +SHA1+. Ignored when using an AEAD cipher like 'aes-256-gcm'.
135
- # * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+.
136
- def initialize(secret, sign_secret = nil, cipher: nil, digest: nil, serializer: nil)
133
+ # ==== Options
134
+ #
135
+ # [+:cipher+]
136
+ # Cipher to use. Can be any cipher returned by +OpenSSL::Cipher.ciphers+.
137
+ # Default is 'aes-256-gcm'.
138
+ #
139
+ # [+:digest+]
140
+ # Digest used for signing. Ignored when using an AEAD cipher like
141
+ # 'aes-256-gcm'.
142
+ #
143
+ # [+:serializer+]
144
+ # The serializer used to serialize message data. You can specify any
145
+ # object that responds to +dump+ and +load+, or you can choose from
146
+ # several preconfigured serializers: +:marshal+, +:json_allow_marshal+,
147
+ # +:json+, +:message_pack_allow_marshal+, +:message_pack+.
148
+ #
149
+ # The preconfigured serializers include a fallback mechanism to support
150
+ # multiple deserialization formats. For example, the +:marshal+ serializer
151
+ # will serialize using +Marshal+, but can deserialize using +Marshal+,
152
+ # ActiveSupport::JSON, or ActiveSupport::MessagePack. This makes it easy
153
+ # to migrate between serializers.
154
+ #
155
+ # The +:marshal+, +:json_allow_marshal+, and +:message_pack_allow_marshal+
156
+ # serializers support deserializing using +Marshal+, but the others do
157
+ # not. Beware that +Marshal+ is a potential vector for deserialization
158
+ # attacks in cases where a message signing secret has been leaked. <em>If
159
+ # possible, choose a serializer that does not support +Marshal+.</em>
160
+ #
161
+ # The +:message_pack+ and +:message_pack_allow_marshal+ serializers use
162
+ # ActiveSupport::MessagePack, which can roundtrip some Ruby types that are
163
+ # not supported by JSON, and may provide improved performance. However,
164
+ # these require the +msgpack+ gem.
165
+ #
166
+ # When using \Rails, the default depends on +config.active_support.message_serializer+.
167
+ # Otherwise, the default is +:marshal+.
168
+ #
169
+ # [+:url_safe+]
170
+ # By default, MessageEncryptor generates RFC 4648 compliant strings
171
+ # which are not URL-safe. In other words, they can contain "+" and "/".
172
+ # If you want to generate URL-safe strings (in compliance with "Base 64
173
+ # Encoding with URL and Filename Safe Alphabet" in RFC 4648), you can
174
+ # pass +true+.
175
+ #
176
+ # [+:force_legacy_metadata_serializer+]
177
+ # Whether to use the legacy metadata serializer, which serializes the
178
+ # message first, then wraps it in an envelope which is also serialized. This
179
+ # was the default in \Rails 7.0 and below.
180
+ #
181
+ # If you don't pass a truthy value, the default is set using
182
+ # +config.active_support.use_message_serializer_for_metadata+.
183
+ def initialize(secret, sign_secret = nil, **options)
184
+ super(**options)
137
185
  @secret = secret
138
- @sign_secret = sign_secret
139
- @cipher = cipher || self.class.default_cipher
140
- @digest = digest || "SHA1" unless aead_mode?
141
- @verifier = resolve_verifier
142
- @serializer = serializer || Marshal
186
+ @cipher = options[:cipher] || self.class.default_cipher
187
+ @aead_mode = new_cipher.authenticated?
188
+ @verifier = if !@aead_mode
189
+ MessageVerifier.new(sign_secret || secret, **options, serializer: NullSerializer)
190
+ end
143
191
  end
144
192
 
145
193
  # Encrypt and sign a message. We need to sign the message in order to avoid
146
194
  # padding attacks. Reference: https://www.limited-entropy.com/padding-oracle-attacks/.
147
- def encrypt_and_sign(value, expires_at: nil, expires_in: nil, purpose: nil)
148
- verifier.generate(_encrypt(value, expires_at: expires_at, expires_in: expires_in, purpose: purpose))
195
+ #
196
+ # ==== Options
197
+ #
198
+ # [+:expires_at+]
199
+ # The datetime at which the message expires. After this datetime,
200
+ # verification of the message will fail.
201
+ #
202
+ # message = encryptor.encrypt_and_sign("hello", expires_at: Time.now.tomorrow)
203
+ # encryptor.decrypt_and_verify(message) # => "hello"
204
+ # # 24 hours later...
205
+ # encryptor.decrypt_and_verify(message) # => nil
206
+ #
207
+ # [+:expires_in+]
208
+ # The duration for which the message is valid. After this duration has
209
+ # elapsed, verification of the message will fail.
210
+ #
211
+ # message = encryptor.encrypt_and_sign("hello", expires_in: 24.hours)
212
+ # encryptor.decrypt_and_verify(message) # => "hello"
213
+ # # 24 hours later...
214
+ # encryptor.decrypt_and_verify(message) # => nil
215
+ #
216
+ # [+:purpose+]
217
+ # The purpose of the message. If specified, the same purpose must be
218
+ # specified when verifying the message; otherwise, verification will fail.
219
+ # (See #decrypt_and_verify.)
220
+ def encrypt_and_sign(value, **options)
221
+ create_message(value, **options)
149
222
  end
150
223
 
151
224
  # Decrypt and verify a message. We need to verify the message in order to
152
225
  # avoid padding attacks. Reference: https://www.limited-entropy.com/padding-oracle-attacks/.
153
- def decrypt_and_verify(data, purpose: nil, **)
154
- _decrypt(verifier.verify(data), purpose)
226
+ #
227
+ # ==== Options
228
+ #
229
+ # [+:purpose+]
230
+ # The purpose that the message was generated with. If the purpose does not
231
+ # match, +decrypt_and_verify+ will return +nil+.
232
+ #
233
+ # message = encryptor.encrypt_and_sign("hello", purpose: "greeting")
234
+ # encryptor.decrypt_and_verify(message, purpose: "greeting") # => "hello"
235
+ # encryptor.decrypt_and_verify(message) # => nil
236
+ #
237
+ # message = encryptor.encrypt_and_sign("bye")
238
+ # encryptor.decrypt_and_verify(message) # => "bye"
239
+ # encryptor.decrypt_and_verify(message, purpose: "greeting") # => nil
240
+ #
241
+ def decrypt_and_verify(message, **options)
242
+ catch_and_raise :invalid_message_format, as: InvalidMessage do
243
+ catch_and_raise :invalid_message_serialization, as: InvalidMessage do
244
+ catch_and_ignore :invalid_message_content do
245
+ read_message(message, **options)
246
+ end
247
+ end
248
+ end
155
249
  end
156
250
 
157
251
  # Given a cipher, returns the key length of the cipher to help generate the key of desired size
@@ -159,8 +253,28 @@ module ActiveSupport
159
253
  OpenSSL::Cipher.new(cipher).key_len
160
254
  end
161
255
 
256
+ def create_message(value, **options) # :nodoc:
257
+ sign(encrypt(serialize_with_metadata(value, **options)))
258
+ end
259
+
260
+ def read_message(message, **options) # :nodoc:
261
+ deserialize_with_metadata(decrypt(verify(message)), **options)
262
+ end
263
+
264
+ def inspect # :nodoc:
265
+ "#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>"
266
+ end
267
+
162
268
  private
163
- def _encrypt(value, **metadata_options)
269
+ def sign(data)
270
+ @verifier ? @verifier.create_message(data) : data
271
+ end
272
+
273
+ def verify(data)
274
+ @verifier ? @verifier.read_message(data) : data
275
+ end
276
+
277
+ def encrypt(data)
164
278
  cipher = new_cipher
165
279
  cipher.encrypt
166
280
  cipher.key = @secret
@@ -169,22 +283,25 @@ module ActiveSupport
169
283
  iv = cipher.random_iv
170
284
  cipher.auth_data = "" if aead_mode?
171
285
 
172
- encrypted_data = cipher.update(Messages::Metadata.wrap(@serializer.dump(value), **metadata_options))
286
+ encrypted_data = cipher.update(data)
173
287
  encrypted_data << cipher.final
174
288
 
175
- blob = "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}"
176
- blob = "#{blob}--#{::Base64.strict_encode64 cipher.auth_tag}" if aead_mode?
177
- blob
289
+ parts = [encrypted_data, iv]
290
+ parts << cipher.auth_tag(AUTH_TAG_LENGTH) if aead_mode?
291
+
292
+ join_parts(parts)
178
293
  end
179
294
 
180
- def _decrypt(encrypted_message, purpose)
295
+ def decrypt(encrypted_message)
181
296
  cipher = new_cipher
182
- encrypted_data, iv, auth_tag = encrypted_message.split("--").map { |v| ::Base64.strict_decode64(v) }
297
+ encrypted_data, iv, auth_tag = extract_parts(encrypted_message)
183
298
 
184
299
  # Currently the OpenSSL bindings do not raise an error if auth_tag is
185
300
  # truncated, which would allow an attacker to easily forge it. See
186
301
  # https://github.com/ruby/openssl/issues/63
187
- raise InvalidMessage if aead_mode? && (auth_tag.nil? || auth_tag.bytes.length != 16)
302
+ if aead_mode? && auth_tag.bytesize != AUTH_TAG_LENGTH
303
+ throw :invalid_message_format, "truncated auth_tag"
304
+ end
188
305
 
189
306
  cipher.decrypt
190
307
  cipher.key = @secret
@@ -196,29 +313,62 @@ module ActiveSupport
196
313
 
197
314
  decrypted_data = cipher.update(encrypted_data)
198
315
  decrypted_data << cipher.final
316
+ rescue OpenSSLCipherError => error
317
+ throw :invalid_message_format, error
318
+ end
199
319
 
200
- message = Messages::Metadata.verify(decrypted_data, purpose)
201
- @serializer.load(message) if message
202
- rescue OpenSSLCipherError, TypeError, ArgumentError
203
- raise InvalidMessage
320
+ def length_after_encode(length_before_encode)
321
+ if @url_safe
322
+ (4 * length_before_encode / 3.0).ceil # length without padding
323
+ else
324
+ 4 * (length_before_encode / 3.0).ceil # length with padding
325
+ end
204
326
  end
205
327
 
206
- def new_cipher
207
- OpenSSL::Cipher.new(@cipher)
328
+ def length_of_encoded_iv
329
+ @length_of_encoded_iv ||= length_after_encode(new_cipher.iv_len)
208
330
  end
209
331
 
210
- attr_reader :verifier
332
+ def length_of_encoded_auth_tag
333
+ @length_of_encoded_auth_tag ||= length_after_encode(AUTH_TAG_LENGTH)
334
+ end
211
335
 
212
- def aead_mode?
213
- @aead_mode ||= new_cipher.authenticated?
336
+ def join_parts(parts)
337
+ parts.map! { |part| encode(part) }.join(SEPARATOR)
214
338
  end
215
339
 
216
- def resolve_verifier
217
- if aead_mode?
218
- NullVerifier
340
+ def extract_part(encrypted_message, rindex, length)
341
+ index = rindex - length
342
+
343
+ if encrypted_message[index - SEPARATOR.length, SEPARATOR.length] == SEPARATOR
344
+ encrypted_message[index, length]
219
345
  else
220
- MessageVerifier.new(@sign_secret || @secret, digest: @digest, serializer: NullSerializer)
346
+ throw :invalid_message_format, "missing separator"
221
347
  end
222
348
  end
349
+
350
+ def extract_parts(encrypted_message)
351
+ parts = []
352
+ rindex = encrypted_message.length
353
+
354
+ if aead_mode?
355
+ parts << extract_part(encrypted_message, rindex, length_of_encoded_auth_tag)
356
+ rindex -= SEPARATOR.length + length_of_encoded_auth_tag
357
+ end
358
+
359
+ parts << extract_part(encrypted_message, rindex, length_of_encoded_iv)
360
+ rindex -= SEPARATOR.length + length_of_encoded_iv
361
+
362
+ parts << encrypted_message[0, rindex]
363
+
364
+ parts.reverse!.map! { |part| decode(part) }
365
+ end
366
+
367
+ def new_cipher
368
+ OpenSSL::Cipher.new(@cipher)
369
+ end
370
+
371
+ attr_reader :aead_mode
372
+ alias :aead_mode? :aead_mode
223
373
  end
224
374
  end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/messages/rotation_coordinator"
4
+
5
+ module ActiveSupport
6
+ class MessageEncryptors < Messages::RotationCoordinator
7
+ ##
8
+ # :attr_accessor: transitional
9
+ #
10
+ # If true, the first two rotation option sets are swapped when building
11
+ # message encryptors. For example, with the following configuration, message
12
+ # encryptors will encrypt messages using <tt>serializer: Marshal, url_safe: true</tt>,
13
+ # and will able to decrypt messages that were encrypted using any of the
14
+ # three option sets:
15
+ #
16
+ # encryptors = ActiveSupport::MessageEncryptors.new { ... }
17
+ # encryptors.rotate(serializer: JSON, url_safe: true)
18
+ # encryptors.rotate(serializer: Marshal, url_safe: true)
19
+ # encryptors.rotate(serializer: Marshal, url_safe: false)
20
+ # encryptors.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
+ # decrypt 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 a
35
+ # +secret_length+ kwarg, and return a suitable secret (string) or secrets
36
+ # (array of strings). +secret_generator+ may also accept other arbitrary
37
+ # kwargs. If #rotate is called with any options matching those kwargs, those
38
+ # options will be passed to +secret_generator+ instead of to the message
39
+ # encryptor.
40
+ #
41
+ # encryptors = ActiveSupport::MessageEncryptors.new do |salt, secret_length:, base:|
42
+ # MySecretGenerator.new(base).generate(salt, secret_length)
43
+ # end
44
+ #
45
+ # encryptors.rotate(base: "...")
46
+
47
+ ##
48
+ # :method: []
49
+ # :call-seq: [](salt)
50
+ #
51
+ # Returns a MessageEncryptor configured with a secret derived from the
52
+ # given +salt+, and options from #rotate. MessageEncryptor instances will
53
+ # be memoized, so the same +salt+ will return the same instance.
54
+
55
+ ##
56
+ # :method: []=
57
+ # :call-seq: []=(salt, encryptor)
58
+ #
59
+ # Overrides a MessageEncryptor instance associated with a given +salt+.
60
+
61
+ ##
62
+ # :method: rotate
63
+ # :call-seq:
64
+ # rotate(**options)
65
+ # rotate(&block)
66
+ #
67
+ # Adds +options+ to the list of option sets. Messages will be encrypted
68
+ # using the first set in the list. When decrypting, however, each set will
69
+ # be tried, in order, until one succeeds.
70
+ #
71
+ # Notably, the +:secret_generator+ option can specify a different secret
72
+ # generator than the one initially specified. The secret generator must
73
+ # respond to +call+, accept a salt and a +secret_length+ kwarg, and return
74
+ # a suitable secret (string) or secrets (array of strings). The secret
75
+ # generator may also accept other arbitrary kwargs.
76
+ #
77
+ # If any options match the kwargs of the operative secret generator, those
78
+ # options will be passed to the secret generator instead of to the message
79
+ # encryptor.
80
+ #
81
+ # For fine-grained per-salt rotations, a block form is supported. The block
82
+ # will receive the salt, and should return an appropriate options Hash. The
83
+ # block may also return +nil+ to indicate that the rotation does not apply
84
+ # to the given salt. For example:
85
+ #
86
+ # encryptors = ActiveSupport::MessageEncryptors.new { ... }
87
+ #
88
+ # encryptors.rotate do |salt|
89
+ # case salt
90
+ # when :foo
91
+ # { serializer: JSON, url_safe: true }
92
+ # when :bar
93
+ # { serializer: Marshal, url_safe: true }
94
+ # end
95
+ # end
96
+ #
97
+ # encryptors.rotate(serializer: Marshal, url_safe: false)
98
+ #
99
+ # # Uses `serializer: JSON, url_safe: true`.
100
+ # # Falls back to `serializer: Marshal, url_safe: false`.
101
+ # encryptors[:foo]
102
+ #
103
+ # # Uses `serializer: Marshal, url_safe: true`.
104
+ # # Falls back to `serializer: Marshal, url_safe: false`.
105
+ # encryptors[:bar]
106
+ #
107
+ # # Uses `serializer: Marshal, url_safe: false`.
108
+ # encryptors[:baz]
109
+
110
+ ##
111
+ # :method: rotate_defaults
112
+ # :call-seq: rotate_defaults
113
+ #
114
+ # Invokes #rotate with the default options.
115
+
116
+ ##
117
+ # :method: clear_rotations
118
+ # :call-seq: clear_rotations
119
+ #
120
+ # Clears the list of option sets.
121
+
122
+ ##
123
+ # :method: on_rotation
124
+ # :call-seq: on_rotation(&callback)
125
+ #
126
+ # Sets a callback to invoke when a message is decrypted using an option set
127
+ # other than the first.
128
+ #
129
+ # For example, this callback could log each time it is called, and thus
130
+ # indicate whether old option sets are still in use or can be removed from
131
+ # rotation.
132
+
133
+ ##
134
+ private
135
+ def build(salt, secret_generator:, secret_generator_options:, **options)
136
+ secret_length = MessageEncryptor.key_len(*options[:cipher])
137
+ secret = secret_generator.call(salt, secret_length: secret_length, **secret_generator_options)
138
+ MessageEncryptor.new(*Array(secret), **options)
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "serializer"
4
+
5
+ module ActiveSupport
6
+ module MessagePack
7
+ module CacheSerializer
8
+ include Serializer
9
+ extend self
10
+
11
+ def load(dumped)
12
+ super
13
+ rescue ActiveSupport::MessagePack::MissingClassError
14
+ # Treat missing class as cache miss => return nil
15
+ end
16
+
17
+ private
18
+ def install_unregistered_type_handler
19
+ Extensions.install_unregistered_type_fallback(message_pack_factory)
20
+ end
21
+ end
22
+ end
23
+ end