activesupport 7.0.8.7 → 7.1.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +722 -314
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +4 -4
  5. data/lib/active_support/actionable_error.rb +3 -1
  6. data/lib/active_support/array_inquirer.rb +2 -0
  7. data/lib/active_support/backtrace_cleaner.rb +25 -5
  8. data/lib/active_support/benchmarkable.rb +1 -0
  9. data/lib/active_support/builder.rb +1 -1
  10. data/lib/active_support/cache/coder.rb +153 -0
  11. data/lib/active_support/cache/entry.rb +128 -0
  12. data/lib/active_support/cache/file_store.rb +36 -9
  13. data/lib/active_support/cache/mem_cache_store.rb +84 -68
  14. data/lib/active_support/cache/memory_store.rb +76 -24
  15. data/lib/active_support/cache/null_store.rb +6 -0
  16. data/lib/active_support/cache/redis_cache_store.rb +126 -131
  17. data/lib/active_support/cache/serializer_with_fallback.rb +175 -0
  18. data/lib/active_support/cache/strategy/local_cache.rb +20 -8
  19. data/lib/active_support/cache.rb +304 -246
  20. data/lib/active_support/callbacks.rb +38 -18
  21. data/lib/active_support/concern.rb +4 -2
  22. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +42 -3
  23. data/lib/active_support/concurrency/null_lock.rb +13 -0
  24. data/lib/active_support/configurable.rb +10 -0
  25. data/lib/active_support/core_ext/array/conversions.rb +2 -1
  26. data/lib/active_support/core_ext/array.rb +0 -1
  27. data/lib/active_support/core_ext/class/subclasses.rb +13 -10
  28. data/lib/active_support/core_ext/date/conversions.rb +1 -0
  29. data/lib/active_support/core_ext/date.rb +0 -1
  30. data/lib/active_support/core_ext/date_and_time/calculations.rb +10 -0
  31. data/lib/active_support/core_ext/date_time/conversions.rb +6 -2
  32. data/lib/active_support/core_ext/date_time.rb +0 -1
  33. data/lib/active_support/core_ext/digest/uuid.rb +1 -10
  34. data/lib/active_support/core_ext/enumerable.rb +3 -75
  35. data/lib/active_support/core_ext/erb/util.rb +196 -0
  36. data/lib/active_support/core_ext/hash/conversions.rb +1 -1
  37. data/lib/active_support/core_ext/module/attribute_accessors.rb +6 -0
  38. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +34 -16
  39. data/lib/active_support/core_ext/module/delegation.rb +40 -11
  40. data/lib/active_support/core_ext/module/deprecation.rb +15 -12
  41. data/lib/active_support/core_ext/module/introspection.rb +0 -1
  42. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  43. data/lib/active_support/core_ext/numeric/conversions.rb +2 -0
  44. data/lib/active_support/core_ext/numeric.rb +0 -1
  45. data/lib/active_support/core_ext/object/deep_dup.rb +16 -0
  46. data/lib/active_support/core_ext/object/duplicable.rb +15 -24
  47. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  48. data/lib/active_support/core_ext/object/instance_variables.rb +22 -12
  49. data/lib/active_support/core_ext/object/json.rb +10 -2
  50. data/lib/active_support/core_ext/object/with.rb +44 -0
  51. data/lib/active_support/core_ext/object/with_options.rb +3 -3
  52. data/lib/active_support/core_ext/object.rb +1 -0
  53. data/lib/active_support/core_ext/pathname/blank.rb +16 -0
  54. data/lib/active_support/core_ext/pathname/existence.rb +2 -0
  55. data/lib/active_support/core_ext/pathname.rb +1 -0
  56. data/lib/active_support/core_ext/range/conversions.rb +28 -7
  57. data/lib/active_support/core_ext/range/{overlaps.rb → overlap.rb} +5 -3
  58. data/lib/active_support/core_ext/range.rb +1 -2
  59. data/lib/active_support/core_ext/securerandom.rb +24 -12
  60. data/lib/active_support/core_ext/string/filters.rb +20 -14
  61. data/lib/active_support/core_ext/string/inflections.rb +16 -5
  62. data/lib/active_support/core_ext/string/output_safety.rb +38 -174
  63. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  64. data/lib/active_support/core_ext/time/calculations.rb +18 -2
  65. data/lib/active_support/core_ext/time/conversions.rb +2 -2
  66. data/lib/active_support/core_ext/time/zones.rb +4 -4
  67. data/lib/active_support/core_ext/time.rb +0 -1
  68. data/lib/active_support/current_attributes.rb +15 -6
  69. data/lib/active_support/dependencies/autoload.rb +17 -12
  70. data/lib/active_support/deprecation/behaviors.rb +53 -32
  71. data/lib/active_support/deprecation/constant_accessor.rb +5 -4
  72. data/lib/active_support/deprecation/deprecators.rb +104 -0
  73. data/lib/active_support/deprecation/disallowed.rb +3 -5
  74. data/lib/active_support/deprecation/instance_delegator.rb +31 -4
  75. data/lib/active_support/deprecation/method_wrappers.rb +6 -23
  76. data/lib/active_support/deprecation/proxy_wrappers.rb +37 -22
  77. data/lib/active_support/deprecation/reporting.rb +35 -21
  78. data/lib/active_support/deprecation.rb +32 -5
  79. data/lib/active_support/deprecator.rb +7 -0
  80. data/lib/active_support/descendants_tracker.rb +104 -132
  81. data/lib/active_support/duration/iso8601_serializer.rb +0 -2
  82. data/lib/active_support/duration.rb +2 -1
  83. data/lib/active_support/encrypted_configuration.rb +30 -9
  84. data/lib/active_support/encrypted_file.rb +8 -3
  85. data/lib/active_support/environment_inquirer.rb +22 -2
  86. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  87. data/lib/active_support/error_reporter.rb +121 -35
  88. data/lib/active_support/execution_wrapper.rb +4 -4
  89. data/lib/active_support/file_update_checker.rb +4 -2
  90. data/lib/active_support/fork_tracker.rb +10 -2
  91. data/lib/active_support/gem_version.rb +4 -4
  92. data/lib/active_support/gzip.rb +2 -0
  93. data/lib/active_support/hash_with_indifferent_access.rb +35 -17
  94. data/lib/active_support/i18n.rb +1 -1
  95. data/lib/active_support/i18n_railtie.rb +20 -13
  96. data/lib/active_support/inflector/inflections.rb +2 -0
  97. data/lib/active_support/inflector/methods.rb +22 -10
  98. data/lib/active_support/inflector/transliterate.rb +3 -1
  99. data/lib/active_support/isolated_execution_state.rb +26 -22
  100. data/lib/active_support/json/decoding.rb +2 -1
  101. data/lib/active_support/json/encoding.rb +25 -43
  102. data/lib/active_support/key_generator.rb +9 -1
  103. data/lib/active_support/lazy_load_hooks.rb +6 -4
  104. data/lib/active_support/locale/en.yml +2 -0
  105. data/lib/active_support/log_subscriber.rb +78 -33
  106. data/lib/active_support/logger.rb +1 -1
  107. data/lib/active_support/logger_thread_safe_level.rb +9 -21
  108. data/lib/active_support/message_encryptor.rb +197 -53
  109. data/lib/active_support/message_encryptors.rb +140 -0
  110. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  111. data/lib/active_support/message_pack/extensions.rb +292 -0
  112. data/lib/active_support/message_pack/serializer.rb +63 -0
  113. data/lib/active_support/message_pack.rb +50 -0
  114. data/lib/active_support/message_verifier.rb +212 -93
  115. data/lib/active_support/message_verifiers.rb +134 -0
  116. data/lib/active_support/messages/codec.rb +65 -0
  117. data/lib/active_support/messages/metadata.rb +111 -45
  118. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  119. data/lib/active_support/messages/rotator.rb +34 -32
  120. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  121. data/lib/active_support/multibyte/chars.rb +2 -0
  122. data/lib/active_support/multibyte/unicode.rb +9 -37
  123. data/lib/active_support/notifications/fanout.rb +239 -81
  124. data/lib/active_support/notifications/instrumenter.rb +71 -14
  125. data/lib/active_support/notifications.rb +1 -1
  126. data/lib/active_support/number_helper/number_converter.rb +2 -2
  127. data/lib/active_support/number_helper/number_to_human_size_converter.rb +1 -1
  128. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -0
  129. data/lib/active_support/ordered_hash.rb +3 -3
  130. data/lib/active_support/ordered_options.rb +14 -0
  131. data/lib/active_support/parameter_filter.rb +84 -69
  132. data/lib/active_support/proxy_object.rb +2 -0
  133. data/lib/active_support/railtie.rb +33 -21
  134. data/lib/active_support/reloader.rb +12 -4
  135. data/lib/active_support/rescuable.rb +2 -0
  136. data/lib/active_support/secure_compare_rotator.rb +16 -9
  137. data/lib/active_support/string_inquirer.rb +3 -1
  138. data/lib/active_support/subscriber.rb +9 -27
  139. data/lib/active_support/syntax_error_proxy.rb +49 -0
  140. data/lib/active_support/tagged_logging.rb +60 -24
  141. data/lib/active_support/test_case.rb +153 -6
  142. data/lib/active_support/testing/assertions.rb +25 -9
  143. data/lib/active_support/testing/autorun.rb +0 -2
  144. data/lib/active_support/testing/constant_stubbing.rb +32 -0
  145. data/lib/active_support/testing/deprecation.rb +25 -25
  146. data/lib/active_support/testing/error_reporter_assertions.rb +108 -0
  147. data/lib/active_support/testing/isolation.rb +1 -1
  148. data/lib/active_support/testing/method_call_assertions.rb +21 -8
  149. data/lib/active_support/testing/parallelize_executor.rb +8 -3
  150. data/lib/active_support/testing/stream.rb +1 -1
  151. data/lib/active_support/testing/strict_warnings.rb +38 -0
  152. data/lib/active_support/testing/time_helpers.rb +32 -14
  153. data/lib/active_support/time_with_zone.rb +4 -14
  154. data/lib/active_support/values/time_zone.rb +9 -7
  155. data/lib/active_support/version.rb +1 -1
  156. data/lib/active_support/xml_mini/jdom.rb +3 -10
  157. data/lib/active_support/xml_mini/nokogiri.rb +1 -1
  158. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  159. data/lib/active_support/xml_mini/rexml.rb +1 -1
  160. data/lib/active_support/xml_mini.rb +2 -2
  161. data/lib/active_support.rb +13 -3
  162. metadata +106 -21
  163. data/lib/active_support/core_ext/array/deprecated_conversions.rb +0 -25
  164. data/lib/active_support/core_ext/date/deprecated_conversions.rb +0 -40
  165. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +0 -36
  166. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +0 -60
  167. data/lib/active_support/core_ext/range/deprecated_conversions.rb +0 -36
  168. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -5
  169. data/lib/active_support/core_ext/time/deprecated_conversions.rb +0 -73
  170. data/lib/active_support/core_ext/uri.rb +0 -5
  171. data/lib/active_support/per_thread_registry.rb +0 -65
@@ -3,10 +3,13 @@
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
  #
@@ -24,7 +27,7 @@ module ActiveSupport
24
27
  # crypt.decrypt_and_verify(encrypted_data) # => "my secret data"
25
28
  #
26
29
  # The +decrypt_and_verify+ method will raise an
27
- # <tt>ActiveSupport::MessageEncryptor::InvalidMessage</tt> exception if the data
30
+ # +ActiveSupport::MessageEncryptor::InvalidMessage+ exception if the data
28
31
  # provided cannot be decrypted or verified.
29
32
  #
30
33
  # crypt.decrypt_and_verify('not encrypted data') # => ActiveSupport::MessageEncryptor::InvalidMessage
@@ -84,8 +87,8 @@ module ActiveSupport
84
87
  # the above should be combined into:
85
88
  #
86
89
  # crypt.rotate old_secret, cipher: "aes-256-cbc"
87
- class MessageEncryptor
88
- prepend Messages::Rotator::Encryptor
90
+ class MessageEncryptor < Messages::Codec
91
+ prepend Messages::Rotator
89
92
 
90
93
  cattr_accessor :use_authenticated_message_encryption, instance_accessor: false, default: false
91
94
 
@@ -109,55 +112,140 @@ module ActiveSupport
109
112
  end
110
113
  end
111
114
 
112
- module NullVerifier # :nodoc:
113
- def self.verify(value)
114
- value
115
- end
116
-
117
- def self.generate(value)
118
- value
119
- end
120
- end
121
-
122
115
  class InvalidMessage < StandardError; end
123
116
  OpenSSLCipherError = OpenSSL::Cipher::CipherError
124
117
 
118
+ AUTH_TAG_LENGTH = 16 # :nodoc:
119
+ SEPARATOR = "--" # :nodoc:
120
+
125
121
  # Initialize a new MessageEncryptor. +secret+ must be at least as long as
126
122
  # the cipher key size. For the default 'aes-256-gcm' cipher, this is 256
127
123
  # bits. If you are using a user-entered secret, you can generate a suitable
128
124
  # key by using ActiveSupport::KeyGenerator or a similar key
129
125
  # derivation function.
130
126
  #
131
- # First additional parameter is used as the signature key for MessageVerifier.
132
- # 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'.
133
130
  #
134
131
  # ActiveSupport::MessageEncryptor.new('secret', 'signature_secret')
135
132
  #
136
- # Options:
137
- # * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by
138
- # <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-gcm'.
139
- # * <tt>:digest</tt> - String of digest to use for signing. Default is
140
- # +SHA1+. Ignored when using an AEAD cipher like 'aes-256-gcm'.
141
- # * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+.
142
- 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)
143
185
  @secret = secret
144
- @sign_secret = sign_secret
145
- @cipher = cipher || self.class.default_cipher
146
- @digest = digest || "SHA1" unless aead_mode?
147
- @verifier = resolve_verifier
148
- @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
149
191
  end
150
192
 
151
193
  # Encrypt and sign a message. We need to sign the message in order to avoid
152
194
  # padding attacks. Reference: https://www.limited-entropy.com/padding-oracle-attacks/.
153
- def encrypt_and_sign(value, expires_at: nil, expires_in: nil, purpose: nil)
154
- 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)
155
222
  end
156
223
 
157
224
  # Decrypt and verify a message. We need to verify the message in order to
158
225
  # avoid padding attacks. Reference: https://www.limited-entropy.com/padding-oracle-attacks/.
159
- def decrypt_and_verify(data, purpose: nil, **)
160
- _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
161
249
  end
162
250
 
163
251
  # Given a cipher, returns the key length of the cipher to help generate the key of desired size
@@ -165,8 +253,28 @@ module ActiveSupport
165
253
  OpenSSL::Cipher.new(cipher).key_len
166
254
  end
167
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
+
168
268
  private
169
- 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)
170
278
  cipher = new_cipher
171
279
  cipher.encrypt
172
280
  cipher.key = @secret
@@ -175,22 +283,25 @@ module ActiveSupport
175
283
  iv = cipher.random_iv
176
284
  cipher.auth_data = "" if aead_mode?
177
285
 
178
- encrypted_data = cipher.update(Messages::Metadata.wrap(@serializer.dump(value), **metadata_options))
286
+ encrypted_data = cipher.update(data)
179
287
  encrypted_data << cipher.final
180
288
 
181
- blob = "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}"
182
- blob = "#{blob}--#{::Base64.strict_encode64 cipher.auth_tag}" if aead_mode?
183
- blob
289
+ parts = [encrypted_data, iv]
290
+ parts << cipher.auth_tag(AUTH_TAG_LENGTH) if aead_mode?
291
+
292
+ join_parts(parts)
184
293
  end
185
294
 
186
- def _decrypt(encrypted_message, purpose)
295
+ def decrypt(encrypted_message)
187
296
  cipher = new_cipher
188
- encrypted_data, iv, auth_tag = encrypted_message.split("--").map { |v| ::Base64.strict_decode64(v) }
297
+ encrypted_data, iv, auth_tag = extract_parts(encrypted_message)
189
298
 
190
299
  # Currently the OpenSSL bindings do not raise an error if auth_tag is
191
300
  # truncated, which would allow an attacker to easily forge it. See
192
301
  # https://github.com/ruby/openssl/issues/63
193
- 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
194
305
 
195
306
  cipher.decrypt
196
307
  cipher.key = @secret
@@ -202,29 +313,62 @@ module ActiveSupport
202
313
 
203
314
  decrypted_data = cipher.update(encrypted_data)
204
315
  decrypted_data << cipher.final
316
+ rescue OpenSSLCipherError => error
317
+ throw :invalid_message_format, error
318
+ end
205
319
 
206
- message = Messages::Metadata.verify(decrypted_data, purpose)
207
- @serializer.load(message) if message
208
- rescue OpenSSLCipherError, TypeError, ArgumentError
209
- 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
210
326
  end
211
327
 
212
- def new_cipher
213
- OpenSSL::Cipher.new(@cipher)
328
+ def length_of_encoded_iv
329
+ @length_of_encoded_iv ||= length_after_encode(new_cipher.iv_len)
214
330
  end
215
331
 
216
- attr_reader :verifier
332
+ def length_of_encoded_auth_tag
333
+ @length_of_encoded_auth_tag ||= length_after_encode(AUTH_TAG_LENGTH)
334
+ end
217
335
 
218
- def aead_mode?
219
- @aead_mode ||= new_cipher.authenticated?
336
+ def join_parts(parts)
337
+ parts.map! { |part| encode(part) }.join(SEPARATOR)
220
338
  end
221
339
 
222
- def resolve_verifier
223
- if aead_mode?
224
- 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]
225
345
  else
226
- MessageVerifier.new(@sign_secret || @secret, digest: @digest, serializer: NullSerializer)
346
+ throw :invalid_message_format, "missing separator"
347
+ end
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
227
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)
228
369
  end
370
+
371
+ attr_reader :aead_mode
372
+ alias :aead_mode? :aead_mode
229
373
  end
230
374
  end
@@ -0,0 +1,140 @@
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
+ private
134
+ def build(salt, secret_generator:, secret_generator_options:, **options)
135
+ secret_length = MessageEncryptor.key_len(*options[:cipher])
136
+ secret = secret_generator.call(salt, secret_length: secret_length, **secret_generator_options)
137
+ MessageEncryptor.new(*Array(secret), **options)
138
+ end
139
+ end
140
+ 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