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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +722 -314
- data/MIT-LICENSE +1 -1
- data/README.rdoc +4 -4
- data/lib/active_support/actionable_error.rb +3 -1
- data/lib/active_support/array_inquirer.rb +2 -0
- data/lib/active_support/backtrace_cleaner.rb +25 -5
- data/lib/active_support/benchmarkable.rb +1 -0
- data/lib/active_support/builder.rb +1 -1
- data/lib/active_support/cache/coder.rb +153 -0
- data/lib/active_support/cache/entry.rb +128 -0
- data/lib/active_support/cache/file_store.rb +36 -9
- data/lib/active_support/cache/mem_cache_store.rb +84 -68
- data/lib/active_support/cache/memory_store.rb +76 -24
- data/lib/active_support/cache/null_store.rb +6 -0
- data/lib/active_support/cache/redis_cache_store.rb +126 -131
- data/lib/active_support/cache/serializer_with_fallback.rb +175 -0
- data/lib/active_support/cache/strategy/local_cache.rb +20 -8
- data/lib/active_support/cache.rb +304 -246
- data/lib/active_support/callbacks.rb +38 -18
- data/lib/active_support/concern.rb +4 -2
- data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +42 -3
- data/lib/active_support/concurrency/null_lock.rb +13 -0
- data/lib/active_support/configurable.rb +10 -0
- data/lib/active_support/core_ext/array/conversions.rb +2 -1
- data/lib/active_support/core_ext/array.rb +0 -1
- data/lib/active_support/core_ext/class/subclasses.rb +13 -10
- data/lib/active_support/core_ext/date/conversions.rb +1 -0
- data/lib/active_support/core_ext/date.rb +0 -1
- data/lib/active_support/core_ext/date_and_time/calculations.rb +10 -0
- data/lib/active_support/core_ext/date_time/conversions.rb +6 -2
- data/lib/active_support/core_ext/date_time.rb +0 -1
- data/lib/active_support/core_ext/digest/uuid.rb +1 -10
- data/lib/active_support/core_ext/enumerable.rb +3 -75
- data/lib/active_support/core_ext/erb/util.rb +196 -0
- data/lib/active_support/core_ext/hash/conversions.rb +1 -1
- data/lib/active_support/core_ext/module/attribute_accessors.rb +6 -0
- data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +34 -16
- data/lib/active_support/core_ext/module/delegation.rb +40 -11
- data/lib/active_support/core_ext/module/deprecation.rb +15 -12
- data/lib/active_support/core_ext/module/introspection.rb +0 -1
- data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
- data/lib/active_support/core_ext/numeric/conversions.rb +2 -0
- data/lib/active_support/core_ext/numeric.rb +0 -1
- data/lib/active_support/core_ext/object/deep_dup.rb +16 -0
- data/lib/active_support/core_ext/object/duplicable.rb +15 -24
- data/lib/active_support/core_ext/object/inclusion.rb +13 -5
- data/lib/active_support/core_ext/object/instance_variables.rb +22 -12
- data/lib/active_support/core_ext/object/json.rb +10 -2
- data/lib/active_support/core_ext/object/with.rb +44 -0
- data/lib/active_support/core_ext/object/with_options.rb +3 -3
- data/lib/active_support/core_ext/object.rb +1 -0
- data/lib/active_support/core_ext/pathname/blank.rb +16 -0
- data/lib/active_support/core_ext/pathname/existence.rb +2 -0
- data/lib/active_support/core_ext/pathname.rb +1 -0
- data/lib/active_support/core_ext/range/conversions.rb +28 -7
- data/lib/active_support/core_ext/range/{overlaps.rb → overlap.rb} +5 -3
- data/lib/active_support/core_ext/range.rb +1 -2
- data/lib/active_support/core_ext/securerandom.rb +24 -12
- data/lib/active_support/core_ext/string/filters.rb +20 -14
- data/lib/active_support/core_ext/string/inflections.rb +16 -5
- data/lib/active_support/core_ext/string/output_safety.rb +38 -174
- data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
- data/lib/active_support/core_ext/time/calculations.rb +18 -2
- data/lib/active_support/core_ext/time/conversions.rb +2 -2
- data/lib/active_support/core_ext/time/zones.rb +4 -4
- data/lib/active_support/core_ext/time.rb +0 -1
- data/lib/active_support/current_attributes.rb +15 -6
- data/lib/active_support/dependencies/autoload.rb +17 -12
- data/lib/active_support/deprecation/behaviors.rb +53 -32
- data/lib/active_support/deprecation/constant_accessor.rb +5 -4
- data/lib/active_support/deprecation/deprecators.rb +104 -0
- data/lib/active_support/deprecation/disallowed.rb +3 -5
- data/lib/active_support/deprecation/instance_delegator.rb +31 -4
- data/lib/active_support/deprecation/method_wrappers.rb +6 -23
- data/lib/active_support/deprecation/proxy_wrappers.rb +37 -22
- data/lib/active_support/deprecation/reporting.rb +35 -21
- data/lib/active_support/deprecation.rb +32 -5
- data/lib/active_support/deprecator.rb +7 -0
- data/lib/active_support/descendants_tracker.rb +104 -132
- data/lib/active_support/duration/iso8601_serializer.rb +0 -2
- data/lib/active_support/duration.rb +2 -1
- data/lib/active_support/encrypted_configuration.rb +30 -9
- data/lib/active_support/encrypted_file.rb +8 -3
- data/lib/active_support/environment_inquirer.rb +22 -2
- data/lib/active_support/error_reporter/test_helper.rb +15 -0
- data/lib/active_support/error_reporter.rb +121 -35
- data/lib/active_support/execution_wrapper.rb +4 -4
- data/lib/active_support/file_update_checker.rb +4 -2
- data/lib/active_support/fork_tracker.rb +10 -2
- data/lib/active_support/gem_version.rb +4 -4
- data/lib/active_support/gzip.rb +2 -0
- data/lib/active_support/hash_with_indifferent_access.rb +35 -17
- data/lib/active_support/i18n.rb +1 -1
- data/lib/active_support/i18n_railtie.rb +20 -13
- data/lib/active_support/inflector/inflections.rb +2 -0
- data/lib/active_support/inflector/methods.rb +22 -10
- data/lib/active_support/inflector/transliterate.rb +3 -1
- data/lib/active_support/isolated_execution_state.rb +26 -22
- data/lib/active_support/json/decoding.rb +2 -1
- data/lib/active_support/json/encoding.rb +25 -43
- data/lib/active_support/key_generator.rb +9 -1
- data/lib/active_support/lazy_load_hooks.rb +6 -4
- data/lib/active_support/locale/en.yml +2 -0
- data/lib/active_support/log_subscriber.rb +78 -33
- data/lib/active_support/logger.rb +1 -1
- data/lib/active_support/logger_thread_safe_level.rb +9 -21
- data/lib/active_support/message_encryptor.rb +197 -53
- data/lib/active_support/message_encryptors.rb +140 -0
- data/lib/active_support/message_pack/cache_serializer.rb +23 -0
- data/lib/active_support/message_pack/extensions.rb +292 -0
- data/lib/active_support/message_pack/serializer.rb +63 -0
- data/lib/active_support/message_pack.rb +50 -0
- data/lib/active_support/message_verifier.rb +212 -93
- data/lib/active_support/message_verifiers.rb +134 -0
- data/lib/active_support/messages/codec.rb +65 -0
- data/lib/active_support/messages/metadata.rb +111 -45
- data/lib/active_support/messages/rotation_coordinator.rb +93 -0
- data/lib/active_support/messages/rotator.rb +34 -32
- data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
- data/lib/active_support/multibyte/chars.rb +2 -0
- data/lib/active_support/multibyte/unicode.rb +9 -37
- data/lib/active_support/notifications/fanout.rb +239 -81
- data/lib/active_support/notifications/instrumenter.rb +71 -14
- data/lib/active_support/notifications.rb +1 -1
- data/lib/active_support/number_helper/number_converter.rb +2 -2
- data/lib/active_support/number_helper/number_to_human_size_converter.rb +1 -1
- data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -0
- data/lib/active_support/ordered_hash.rb +3 -3
- data/lib/active_support/ordered_options.rb +14 -0
- data/lib/active_support/parameter_filter.rb +84 -69
- data/lib/active_support/proxy_object.rb +2 -0
- data/lib/active_support/railtie.rb +33 -21
- data/lib/active_support/reloader.rb +12 -4
- data/lib/active_support/rescuable.rb +2 -0
- data/lib/active_support/secure_compare_rotator.rb +16 -9
- data/lib/active_support/string_inquirer.rb +3 -1
- data/lib/active_support/subscriber.rb +9 -27
- data/lib/active_support/syntax_error_proxy.rb +49 -0
- data/lib/active_support/tagged_logging.rb +60 -24
- data/lib/active_support/test_case.rb +153 -6
- data/lib/active_support/testing/assertions.rb +25 -9
- data/lib/active_support/testing/autorun.rb +0 -2
- data/lib/active_support/testing/constant_stubbing.rb +32 -0
- data/lib/active_support/testing/deprecation.rb +25 -25
- data/lib/active_support/testing/error_reporter_assertions.rb +108 -0
- data/lib/active_support/testing/isolation.rb +1 -1
- data/lib/active_support/testing/method_call_assertions.rb +21 -8
- data/lib/active_support/testing/parallelize_executor.rb +8 -3
- data/lib/active_support/testing/stream.rb +1 -1
- data/lib/active_support/testing/strict_warnings.rb +38 -0
- data/lib/active_support/testing/time_helpers.rb +32 -14
- data/lib/active_support/time_with_zone.rb +4 -14
- data/lib/active_support/values/time_zone.rb +9 -7
- data/lib/active_support/version.rb +1 -1
- data/lib/active_support/xml_mini/jdom.rb +3 -10
- data/lib/active_support/xml_mini/nokogiri.rb +1 -1
- data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
- data/lib/active_support/xml_mini/rexml.rb +1 -1
- data/lib/active_support/xml_mini.rb +2 -2
- data/lib/active_support.rb +13 -3
- metadata +106 -21
- data/lib/active_support/core_ext/array/deprecated_conversions.rb +0 -25
- data/lib/active_support/core_ext/date/deprecated_conversions.rb +0 -40
- data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +0 -36
- data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +0 -60
- data/lib/active_support/core_ext/range/deprecated_conversions.rb +0 -36
- data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -5
- data/lib/active_support/core_ext/time/deprecated_conversions.rb +0 -73
- data/lib/active_support/core_ext/uri.rb +0 -5
- 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
|
-
#
|
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
|
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
|
-
#
|
132
|
-
# This allows you to specify keys to encrypt and sign
|
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
|
-
#
|
138
|
-
#
|
139
|
-
#
|
140
|
-
#
|
141
|
-
#
|
142
|
-
|
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
|
-
@
|
145
|
-
@
|
146
|
-
@
|
147
|
-
|
148
|
-
|
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
|
-
|
154
|
-
|
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
|
-
|
160
|
-
|
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
|
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(
|
286
|
+
encrypted_data = cipher.update(data)
|
179
287
|
encrypted_data << cipher.final
|
180
288
|
|
181
|
-
|
182
|
-
|
183
|
-
|
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
|
295
|
+
def decrypt(encrypted_message)
|
187
296
|
cipher = new_cipher
|
188
|
-
encrypted_data, iv, auth_tag = encrypted_message
|
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
|
-
|
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
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
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
|
213
|
-
|
328
|
+
def length_of_encoded_iv
|
329
|
+
@length_of_encoded_iv ||= length_after_encode(new_cipher.iv_len)
|
214
330
|
end
|
215
331
|
|
216
|
-
|
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
|
219
|
-
|
336
|
+
def join_parts(parts)
|
337
|
+
parts.map! { |part| encode(part) }.join(SEPARATOR)
|
220
338
|
end
|
221
339
|
|
222
|
-
def
|
223
|
-
|
224
|
-
|
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
|
-
|
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
|