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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1075 -325
- data/MIT-LICENSE +1 -1
- data/README.rdoc +7 -7
- data/lib/active_support/actionable_error.rb +4 -2
- data/lib/active_support/array_inquirer.rb +2 -2
- data/lib/active_support/backtrace_cleaner.rb +32 -7
- data/lib/active_support/benchmarkable.rb +3 -2
- data/lib/active_support/broadcast_logger.rb +251 -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 +134 -0
- data/lib/active_support/cache/file_store.rb +53 -20
- data/lib/active_support/cache/mem_cache_store.rb +201 -62
- data/lib/active_support/cache/memory_store.rb +86 -24
- data/lib/active_support/cache/null_store.rb +16 -2
- data/lib/active_support/cache/redis_cache_store.rb +186 -193
- data/lib/active_support/cache/serializer_with_fallback.rb +175 -0
- data/lib/active_support/cache/strategy/local_cache.rb +63 -71
- data/lib/active_support/cache.rb +487 -249
- data/lib/active_support/callbacks.rb +227 -105
- data/lib/active_support/code_generator.rb +70 -0
- data/lib/active_support/concern.rb +9 -7
- data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +44 -7
- data/lib/active_support/concurrency/null_lock.rb +13 -0
- data/lib/active_support/concurrency/share_lock.rb +2 -2
- data/lib/active_support/configurable.rb +18 -5
- data/lib/active_support/configuration_file.rb +7 -2
- data/lib/active_support/core_ext/array/access.rb +1 -5
- data/lib/active_support/core_ext/array/conversions.rb +15 -13
- data/lib/active_support/core_ext/array/grouping.rb +6 -6
- data/lib/active_support/core_ext/array/inquiry.rb +2 -2
- data/lib/active_support/core_ext/big_decimal/conversions.rb +1 -1
- data/lib/active_support/core_ext/class/subclasses.rb +37 -26
- data/lib/active_support/core_ext/date/blank.rb +1 -1
- data/lib/active_support/core_ext/date/calculations.rb +24 -9
- data/lib/active_support/core_ext/date/conversions.rb +16 -15
- data/lib/active_support/core_ext/date_and_time/calculations.rb +14 -4
- data/lib/active_support/core_ext/date_and_time/compatibility.rb +1 -1
- data/lib/active_support/core_ext/date_time/blank.rb +1 -1
- data/lib/active_support/core_ext/date_time/calculations.rb +4 -0
- data/lib/active_support/core_ext/date_time/conversions.rb +19 -15
- data/lib/active_support/core_ext/digest/uuid.rb +30 -13
- data/lib/active_support/core_ext/enumerable.rb +85 -83
- data/lib/active_support/core_ext/erb/util.rb +196 -0
- data/lib/active_support/core_ext/file/atomic.rb +3 -1
- data/lib/active_support/core_ext/hash/conversions.rb +1 -2
- data/lib/active_support/core_ext/hash/deep_merge.rb +22 -14
- data/lib/active_support/core_ext/hash/deep_transform_values.rb +3 -3
- data/lib/active_support/core_ext/hash/indifferent_access.rb +3 -3
- data/lib/active_support/core_ext/hash/keys.rb +4 -4
- data/lib/active_support/core_ext/integer/inflections.rb +12 -12
- data/lib/active_support/core_ext/kernel/reporting.rb +4 -4
- data/lib/active_support/core_ext/kernel/singleton_class.rb +1 -1
- data/lib/active_support/core_ext/module/attribute_accessors.rb +8 -0
- data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +49 -22
- data/lib/active_support/core_ext/module/concerning.rb +6 -6
- data/lib/active_support/core_ext/module/delegation.rb +81 -43
- 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/name_error.rb +2 -8
- data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
- data/lib/active_support/core_ext/numeric/conversions.rb +82 -77
- data/lib/active_support/core_ext/object/acts_like.rb +29 -5
- data/lib/active_support/core_ext/object/blank.rb +2 -2
- data/lib/active_support/core_ext/object/deep_dup.rb +17 -1
- data/lib/active_support/core_ext/object/duplicable.rb +31 -11
- 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 +49 -27
- data/lib/active_support/core_ext/object/to_query.rb +2 -4
- data/lib/active_support/core_ext/object/try.rb +20 -20
- data/lib/active_support/core_ext/object/with.rb +44 -0
- data/lib/active_support/core_ext/object/with_options.rb +25 -6
- 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 +23 -0
- data/lib/active_support/core_ext/pathname.rb +4 -0
- data/lib/active_support/core_ext/range/compare_range.rb +0 -25
- data/lib/active_support/core_ext/range/conversions.rb +34 -13
- data/lib/active_support/core_ext/range/each.rb +1 -1
- data/lib/active_support/core_ext/range/overlap.rb +40 -0
- data/lib/active_support/core_ext/range.rb +1 -2
- data/lib/active_support/core_ext/securerandom.rb +25 -13
- data/lib/active_support/core_ext/string/conversions.rb +2 -2
- data/lib/active_support/core_ext/string/filters.rb +21 -15
- data/lib/active_support/core_ext/string/indent.rb +1 -1
- data/lib/active_support/core_ext/string/inflections.rb +17 -10
- data/lib/active_support/core_ext/string/inquiry.rb +1 -1
- data/lib/active_support/core_ext/string/output_safety.rb +85 -165
- data/lib/active_support/core_ext/symbol/starts_ends_with.rb +0 -8
- data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
- data/lib/active_support/core_ext/time/calculations.rb +30 -8
- data/lib/active_support/core_ext/time/conversions.rb +15 -13
- data/lib/active_support/core_ext/time/zones.rb +12 -28
- data/lib/active_support/core_ext.rb +2 -1
- data/lib/active_support/current_attributes.rb +47 -20
- data/lib/active_support/deep_mergeable.rb +53 -0
- data/lib/active_support/dependencies/autoload.rb +17 -12
- data/lib/active_support/dependencies/interlock.rb +10 -18
- data/lib/active_support/dependencies/require_dependency.rb +28 -0
- data/lib/active_support/dependencies.rb +58 -788
- data/lib/active_support/deprecation/behaviors.rb +66 -40
- 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 +6 -8
- data/lib/active_support/deprecation/instance_delegator.rb +31 -4
- data/lib/active_support/deprecation/method_wrappers.rb +9 -26
- data/lib/active_support/deprecation/proxy_wrappers.rb +38 -23
- data/lib/active_support/deprecation/reporting.rb +43 -26
- data/lib/active_support/deprecation.rb +32 -5
- data/lib/active_support/deprecator.rb +7 -0
- data/lib/active_support/descendants_tracker.rb +150 -72
- data/lib/active_support/digest.rb +5 -3
- data/lib/active_support/duration/iso8601_parser.rb +3 -3
- data/lib/active_support/duration/iso8601_serializer.rb +9 -3
- data/lib/active_support/duration.rb +83 -52
- data/lib/active_support/encrypted_configuration.rb +72 -9
- data/lib/active_support/encrypted_file.rb +29 -13
- data/lib/active_support/environment_inquirer.rb +23 -3
- data/lib/active_support/error_reporter/test_helper.rb +15 -0
- data/lib/active_support/error_reporter.rb +203 -0
- data/lib/active_support/evented_file_update_checker.rb +20 -7
- data/lib/active_support/execution_context/test_helper.rb +13 -0
- data/lib/active_support/execution_context.rb +53 -0
- data/lib/active_support/execution_wrapper.rb +44 -22
- data/lib/active_support/executor/test_helper.rb +7 -0
- data/lib/active_support/file_update_checker.rb +4 -2
- data/lib/active_support/fork_tracker.rb +28 -11
- 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 +44 -19
- data/lib/active_support/html_safe_translation.rb +53 -0
- data/lib/active_support/i18n.rb +2 -1
- data/lib/active_support/i18n_railtie.rb +21 -14
- data/lib/active_support/inflector/inflections.rb +25 -7
- data/lib/active_support/inflector/methods.rb +50 -64
- data/lib/active_support/inflector/transliterate.rb +4 -2
- data/lib/active_support/isolated_execution_state.rb +76 -0
- data/lib/active_support/json/decoding.rb +2 -1
- data/lib/active_support/json/encoding.rb +27 -45
- data/lib/active_support/key_generator.rb +31 -6
- data/lib/active_support/lazy_load_hooks.rb +33 -7
- data/lib/active_support/locale/en.yml +4 -2
- data/lib/active_support/log_subscriber/test_helper.rb +2 -2
- data/lib/active_support/log_subscriber.rb +97 -35
- data/lib/active_support/logger.rb +9 -60
- data/lib/active_support/logger_thread_safe_level.rb +11 -34
- data/lib/active_support/message_encryptor.rb +206 -56
- data/lib/active_support/message_encryptors.rb +141 -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 +235 -84
- data/lib/active_support/message_verifiers.rb +135 -0
- data/lib/active_support/messages/codec.rb +65 -0
- data/lib/active_support/messages/metadata.rb +112 -46
- 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 +12 -11
- data/lib/active_support/multibyte/unicode.rb +9 -49
- data/lib/active_support/multibyte.rb +1 -1
- data/lib/active_support/notifications/fanout.rb +304 -114
- data/lib/active_support/notifications/instrumenter.rb +117 -35
- data/lib/active_support/notifications.rb +25 -25
- data/lib/active_support/number_helper/number_converter.rb +14 -7
- data/lib/active_support/number_helper/number_to_currency_converter.rb +11 -6
- data/lib/active_support/number_helper/number_to_delimited_converter.rb +1 -1
- data/lib/active_support/number_helper/number_to_human_size_converter.rb +4 -4
- data/lib/active_support/number_helper/number_to_phone_converter.rb +2 -1
- data/lib/active_support/number_helper/number_to_rounded_converter.rb +10 -6
- data/lib/active_support/number_helper/rounding_helper.rb +2 -6
- data/lib/active_support/number_helper.rb +379 -319
- data/lib/active_support/option_merger.rb +10 -18
- data/lib/active_support/ordered_hash.rb +4 -4
- data/lib/active_support/ordered_options.rb +15 -1
- data/lib/active_support/parameter_filter.rb +105 -81
- data/lib/active_support/proxy_object.rb +2 -0
- data/lib/active_support/railtie.rb +83 -21
- data/lib/active_support/reloader.rb +13 -5
- data/lib/active_support/rescuable.rb +18 -16
- data/lib/active_support/ruby_features.rb +7 -0
- data/lib/active_support/secure_compare_rotator.rb +18 -11
- data/lib/active_support/security_utils.rb +1 -1
- data/lib/active_support/string_inquirer.rb +3 -3
- data/lib/active_support/subscriber.rb +11 -40
- data/lib/active_support/syntax_error_proxy.rb +60 -0
- data/lib/active_support/tagged_logging.rb +65 -25
- data/lib/active_support/test_case.rb +166 -27
- data/lib/active_support/testing/assertions.rb +61 -15
- 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 +53 -2
- data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
- data/lib/active_support/testing/isolation.rb +30 -29
- data/lib/active_support/testing/method_call_assertions.rb +24 -11
- data/lib/active_support/testing/parallelization/server.rb +4 -0
- data/lib/active_support/testing/parallelization/worker.rb +3 -0
- data/lib/active_support/testing/parallelization.rb +4 -0
- data/lib/active_support/testing/parallelize_executor.rb +81 -0
- data/lib/active_support/testing/setup_and_teardown.rb +2 -0
- data/lib/active_support/testing/stream.rb +4 -6
- data/lib/active_support/testing/strict_warnings.rb +39 -0
- data/lib/active_support/testing/tagged_logging.rb +1 -1
- data/lib/active_support/testing/time_helpers.rb +49 -16
- data/lib/active_support/time_with_zone.rb +39 -28
- data/lib/active_support/values/time_zone.rb +50 -18
- data/lib/active_support/version.rb +1 -1
- data/lib/active_support/xml_mini/jdom.rb +4 -11
- data/lib/active_support/xml_mini/libxml.rb +5 -5
- data/lib/active_support/xml_mini/libxmlsax.rb +1 -1
- data/lib/active_support/xml_mini/nokogiri.rb +5 -5
- data/lib/active_support/xml_mini/nokogirisax.rb +2 -2
- data/lib/active_support/xml_mini/rexml.rb +2 -2
- data/lib/active_support/xml_mini.rb +7 -6
- data/lib/active_support.rb +28 -1
- metadata +150 -18
- data/lib/active_support/core_ext/marshal.rb +0 -26
- data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -28
- data/lib/active_support/core_ext/range/overlaps.rb +0 -10
- data/lib/active_support/core_ext/uri.rb +0 -29
- data/lib/active_support/dependencies/zeitwerk_integration.rb +0 -117
- 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 "
|
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
|
-
|
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
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
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
|
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
|
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
|
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
|
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
|
124
|
+
# key by using ActiveSupport::KeyGenerator or a similar key
|
123
125
|
# derivation function.
|
124
126
|
#
|
125
|
-
#
|
126
|
-
# 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'.
|
127
130
|
#
|
128
131
|
# ActiveSupport::MessageEncryptor.new('secret', 'signature_secret')
|
129
132
|
#
|
130
|
-
# Options
|
131
|
-
#
|
132
|
-
#
|
133
|
-
#
|
134
|
-
#
|
135
|
-
#
|
136
|
-
|
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
|
-
@
|
139
|
-
@
|
140
|
-
@
|
141
|
-
|
142
|
-
|
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
|
-
|
148
|
-
|
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
|
-
|
154
|
-
|
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
|
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(
|
286
|
+
encrypted_data = cipher.update(data)
|
173
287
|
encrypted_data << cipher.final
|
174
288
|
|
175
|
-
|
176
|
-
|
177
|
-
|
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
|
295
|
+
def decrypt(encrypted_message)
|
181
296
|
cipher = new_cipher
|
182
|
-
encrypted_data, iv, auth_tag = encrypted_message
|
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
|
-
|
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
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
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
|
207
|
-
|
328
|
+
def length_of_encoded_iv
|
329
|
+
@length_of_encoded_iv ||= length_after_encode(new_cipher.iv_len)
|
208
330
|
end
|
209
331
|
|
210
|
-
|
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
|
213
|
-
|
336
|
+
def join_parts(parts)
|
337
|
+
parts.map! { |part| encode(part) }.join(SEPARATOR)
|
214
338
|
end
|
215
339
|
|
216
|
-
def
|
217
|
-
|
218
|
-
|
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
|
-
|
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
|