activesupport 5.1.7 → 7.0.4.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activesupport might be problematic. Click here for more details.

Files changed (279) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +259 -585
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -5
  5. data/lib/active_support/actionable_error.rb +48 -0
  6. data/lib/active_support/all.rb +2 -0
  7. data/lib/active_support/array_inquirer.rb +4 -2
  8. data/lib/active_support/backtrace_cleaner.rb +33 -5
  9. data/lib/active_support/benchmarkable.rb +5 -3
  10. data/lib/active_support/builder.rb +2 -0
  11. data/lib/active_support/cache/file_store.rb +50 -43
  12. data/lib/active_support/cache/mem_cache_store.rb +194 -67
  13. data/lib/active_support/cache/memory_store.rb +70 -34
  14. data/lib/active_support/cache/null_store.rb +18 -3
  15. data/lib/active_support/cache/redis_cache_store.rb +474 -0
  16. data/lib/active_support/cache/strategy/local_cache.rb +73 -50
  17. data/lib/active_support/cache/strategy/local_cache_middleware.rb +2 -0
  18. data/lib/active_support/cache.rb +556 -220
  19. data/lib/active_support/callbacks.rb +264 -159
  20. data/lib/active_support/code_generator.rb +65 -0
  21. data/lib/active_support/concern.rb +81 -8
  22. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +16 -0
  23. data/lib/active_support/concurrency/share_lock.rb +4 -3
  24. data/lib/active_support/configurable.rb +17 -16
  25. data/lib/active_support/configuration_file.rb +51 -0
  26. data/lib/active_support/core_ext/array/access.rb +18 -8
  27. data/lib/active_support/core_ext/array/conversions.rb +20 -17
  28. data/lib/active_support/core_ext/array/deprecated_conversions.rb +25 -0
  29. data/lib/active_support/core_ext/array/extract.rb +21 -0
  30. data/lib/active_support/core_ext/array/extract_options.rb +2 -0
  31. data/lib/active_support/core_ext/array/grouping.rb +8 -6
  32. data/lib/active_support/core_ext/array/inquiry.rb +4 -2
  33. data/lib/active_support/core_ext/array/wrap.rb +2 -0
  34. data/lib/active_support/core_ext/array.rb +4 -1
  35. data/lib/active_support/core_ext/benchmark.rb +4 -2
  36. data/lib/active_support/core_ext/big_decimal/conversions.rb +3 -1
  37. data/lib/active_support/core_ext/big_decimal.rb +2 -0
  38. data/lib/active_support/core_ext/class/attribute.rb +50 -47
  39. data/lib/active_support/core_ext/class/attribute_accessors.rb +2 -0
  40. data/lib/active_support/core_ext/class/subclasses.rb +10 -24
  41. data/lib/active_support/core_ext/class.rb +2 -0
  42. data/lib/active_support/core_ext/date/acts_like.rb +2 -0
  43. data/lib/active_support/core_ext/date/blank.rb +3 -1
  44. data/lib/active_support/core_ext/date/calculations.rb +17 -14
  45. data/lib/active_support/core_ext/date/conversions.rb +24 -22
  46. data/lib/active_support/core_ext/date/deprecated_conversions.rb +26 -0
  47. data/lib/active_support/core_ext/date/zones.rb +2 -0
  48. data/lib/active_support/core_ext/date.rb +3 -0
  49. data/lib/active_support/core_ext/date_and_time/calculations.rb +65 -41
  50. data/lib/active_support/core_ext/date_and_time/compatibility.rb +18 -1
  51. data/lib/active_support/core_ext/date_and_time/zones.rb +2 -1
  52. data/lib/active_support/core_ext/date_time/acts_like.rb +2 -0
  53. data/lib/active_support/core_ext/date_time/blank.rb +3 -1
  54. data/lib/active_support/core_ext/date_time/calculations.rb +3 -1
  55. data/lib/active_support/core_ext/date_time/compatibility.rb +7 -5
  56. data/lib/active_support/core_ext/date_time/conversions.rb +15 -14
  57. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +22 -0
  58. data/lib/active_support/core_ext/date_time.rb +3 -0
  59. data/lib/active_support/core_ext/digest/uuid.rb +42 -14
  60. data/lib/active_support/core_ext/digest.rb +3 -0
  61. data/lib/active_support/core_ext/enumerable.rb +244 -72
  62. data/lib/active_support/core_ext/file/atomic.rb +6 -2
  63. data/lib/active_support/core_ext/file.rb +2 -0
  64. data/lib/active_support/core_ext/hash/conversions.rb +7 -6
  65. data/lib/active_support/core_ext/hash/deep_merge.rb +8 -12
  66. data/lib/active_support/core_ext/hash/deep_transform_values.rb +46 -0
  67. data/lib/active_support/core_ext/hash/except.rb +4 -2
  68. data/lib/active_support/core_ext/hash/indifferent_access.rb +5 -3
  69. data/lib/active_support/core_ext/hash/keys.rb +4 -31
  70. data/lib/active_support/core_ext/hash/reverse_merge.rb +5 -2
  71. data/lib/active_support/core_ext/hash/slice.rb +8 -29
  72. data/lib/active_support/core_ext/hash.rb +3 -2
  73. data/lib/active_support/core_ext/integer/inflections.rb +2 -0
  74. data/lib/active_support/core_ext/integer/multiple.rb +3 -1
  75. data/lib/active_support/core_ext/integer/time.rb +7 -14
  76. data/lib/active_support/core_ext/integer.rb +2 -0
  77. data/lib/active_support/core_ext/kernel/concern.rb +2 -0
  78. data/lib/active_support/core_ext/kernel/reporting.rb +6 -4
  79. data/lib/active_support/core_ext/kernel/singleton_class.rb +3 -1
  80. data/lib/active_support/core_ext/kernel.rb +2 -1
  81. data/lib/active_support/core_ext/load_error.rb +3 -8
  82. data/lib/active_support/core_ext/module/aliasing.rb +2 -0
  83. data/lib/active_support/core_ext/module/anonymous.rb +2 -0
  84. data/lib/active_support/core_ext/module/attr_internal.rb +4 -2
  85. data/lib/active_support/core_ext/module/attribute_accessors.rb +46 -56
  86. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +36 -27
  87. data/lib/active_support/core_ext/module/concerning.rb +15 -10
  88. data/lib/active_support/core_ext/module/delegation.rb +97 -58
  89. data/lib/active_support/core_ext/module/deprecation.rb +2 -0
  90. data/lib/active_support/core_ext/module/introspection.rb +18 -15
  91. data/lib/active_support/core_ext/module/redefine_method.rb +40 -0
  92. data/lib/active_support/core_ext/module/remove_method.rb +5 -23
  93. data/lib/active_support/core_ext/module.rb +3 -1
  94. data/lib/active_support/core_ext/name_error.rb +30 -2
  95. data/lib/active_support/core_ext/numeric/bytes.rb +2 -0
  96. data/lib/active_support/core_ext/numeric/conversions.rb +134 -129
  97. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +60 -0
  98. data/lib/active_support/core_ext/numeric/time.rb +7 -15
  99. data/lib/active_support/core_ext/numeric.rb +3 -1
  100. data/lib/active_support/core_ext/object/acts_like.rb +41 -6
  101. data/lib/active_support/core_ext/object/blank.rb +15 -5
  102. data/lib/active_support/core_ext/object/conversions.rb +2 -0
  103. data/lib/active_support/core_ext/object/deep_dup.rb +3 -1
  104. data/lib/active_support/core_ext/object/duplicable.rb +16 -110
  105. data/lib/active_support/core_ext/object/inclusion.rb +2 -0
  106. data/lib/active_support/core_ext/object/instance_variables.rb +2 -0
  107. data/lib/active_support/core_ext/object/json.rb +51 -26
  108. data/lib/active_support/core_ext/object/to_param.rb +2 -0
  109. data/lib/active_support/core_ext/object/to_query.rb +4 -2
  110. data/lib/active_support/core_ext/object/try.rb +26 -14
  111. data/lib/active_support/core_ext/object/with_options.rb +24 -3
  112. data/lib/active_support/core_ext/object.rb +2 -0
  113. data/lib/active_support/core_ext/pathname/existence.rb +21 -0
  114. data/lib/active_support/core_ext/pathname.rb +3 -0
  115. data/lib/active_support/core_ext/range/compare_range.rb +57 -0
  116. data/lib/active_support/core_ext/range/conversions.rb +35 -25
  117. data/lib/active_support/core_ext/range/deprecated_conversions.rb +26 -0
  118. data/lib/active_support/core_ext/range/each.rb +6 -3
  119. data/lib/active_support/core_ext/range/include_time_with_zone.rb +7 -0
  120. data/lib/active_support/core_ext/range/overlaps.rb +3 -1
  121. data/lib/active_support/core_ext/range.rb +4 -1
  122. data/lib/active_support/core_ext/regexp.rb +10 -5
  123. data/lib/active_support/core_ext/securerandom.rb +25 -3
  124. data/lib/active_support/core_ext/string/access.rb +7 -16
  125. data/lib/active_support/core_ext/string/behavior.rb +2 -0
  126. data/lib/active_support/core_ext/string/conversions.rb +5 -2
  127. data/lib/active_support/core_ext/string/exclude.rb +2 -0
  128. data/lib/active_support/core_ext/string/filters.rb +44 -1
  129. data/lib/active_support/core_ext/string/indent.rb +2 -0
  130. data/lib/active_support/core_ext/string/inflections.rb +69 -16
  131. data/lib/active_support/core_ext/string/inquiry.rb +4 -1
  132. data/lib/active_support/core_ext/string/multibyte.rb +9 -4
  133. data/lib/active_support/core_ext/string/output_safety.rb +135 -27
  134. data/lib/active_support/core_ext/string/starts_ends_with.rb +4 -2
  135. data/lib/active_support/core_ext/string/strip.rb +5 -1
  136. data/lib/active_support/core_ext/string/zones.rb +2 -0
  137. data/lib/active_support/core_ext/string.rb +2 -0
  138. data/lib/active_support/core_ext/symbol/starts_ends_with.rb +6 -0
  139. data/lib/active_support/core_ext/symbol.rb +3 -0
  140. data/lib/active_support/core_ext/time/acts_like.rb +2 -0
  141. data/lib/active_support/core_ext/time/calculations.rb +81 -24
  142. data/lib/active_support/core_ext/time/compatibility.rb +4 -2
  143. data/lib/active_support/core_ext/time/conversions.rb +17 -12
  144. data/lib/active_support/core_ext/time/deprecated_conversions.rb +22 -0
  145. data/lib/active_support/core_ext/time/zones.rb +12 -25
  146. data/lib/active_support/core_ext/time.rb +3 -0
  147. data/lib/active_support/core_ext/uri.rb +4 -23
  148. data/lib/active_support/core_ext.rb +4 -1
  149. data/lib/active_support/current_attributes/test_helper.rb +13 -0
  150. data/lib/active_support/current_attributes.rb +226 -0
  151. data/lib/active_support/dependencies/autoload.rb +2 -0
  152. data/lib/active_support/dependencies/interlock.rb +12 -18
  153. data/lib/active_support/dependencies/require_dependency.rb +28 -0
  154. data/lib/active_support/dependencies.rb +59 -715
  155. data/lib/active_support/deprecation/behaviors.rb +48 -13
  156. data/lib/active_support/deprecation/constant_accessor.rb +4 -2
  157. data/lib/active_support/deprecation/disallowed.rb +56 -0
  158. data/lib/active_support/deprecation/instance_delegator.rb +2 -1
  159. data/lib/active_support/deprecation/method_wrappers.rb +29 -21
  160. data/lib/active_support/deprecation/proxy_wrappers.rb +34 -8
  161. data/lib/active_support/deprecation/reporting.rb +54 -9
  162. data/lib/active_support/deprecation.rb +10 -3
  163. data/lib/active_support/descendants_tracker.rb +192 -34
  164. data/lib/active_support/digest.rb +22 -0
  165. data/lib/active_support/duration/iso8601_parser.rb +9 -9
  166. data/lib/active_support/duration/iso8601_serializer.rb +29 -15
  167. data/lib/active_support/duration.rb +158 -72
  168. data/lib/active_support/encrypted_configuration.rb +56 -0
  169. data/lib/active_support/encrypted_file.rb +129 -0
  170. data/lib/active_support/environment_inquirer.rb +20 -0
  171. data/lib/active_support/error_reporter.rb +117 -0
  172. data/lib/active_support/evented_file_update_checker.rb +87 -122
  173. data/lib/active_support/execution_context/test_helper.rb +13 -0
  174. data/lib/active_support/execution_context.rb +53 -0
  175. data/lib/active_support/execution_wrapper.rb +46 -21
  176. data/lib/active_support/executor/test_helper.rb +7 -0
  177. data/lib/active_support/executor.rb +2 -0
  178. data/lib/active_support/file_update_checker.rb +2 -1
  179. data/lib/active_support/fork_tracker.rb +71 -0
  180. data/lib/active_support/gem_version.rb +7 -5
  181. data/lib/active_support/gzip.rb +2 -0
  182. data/lib/active_support/hash_with_indifferent_access.rb +126 -42
  183. data/lib/active_support/html_safe_translation.rb +43 -0
  184. data/lib/active_support/i18n.rb +5 -1
  185. data/lib/active_support/i18n_railtie.rb +19 -14
  186. data/lib/active_support/inflections.rb +2 -0
  187. data/lib/active_support/inflector/inflections.rb +41 -14
  188. data/lib/active_support/inflector/methods.rb +73 -87
  189. data/lib/active_support/inflector/transliterate.rb +56 -18
  190. data/lib/active_support/inflector.rb +2 -0
  191. data/lib/active_support/isolated_execution_state.rb +72 -0
  192. data/lib/active_support/json/decoding.rb +27 -26
  193. data/lib/active_support/json/encoding.rb +16 -6
  194. data/lib/active_support/json.rb +2 -0
  195. data/lib/active_support/key_generator.rb +25 -38
  196. data/lib/active_support/lazy_load_hooks.rb +35 -6
  197. data/lib/active_support/locale/en.rb +33 -0
  198. data/lib/active_support/locale/en.yml +8 -4
  199. data/lib/active_support/log_subscriber/test_helper.rb +4 -2
  200. data/lib/active_support/log_subscriber.rb +54 -13
  201. data/lib/active_support/logger.rb +4 -17
  202. data/lib/active_support/logger_silence.rb +13 -20
  203. data/lib/active_support/logger_thread_safe_level.rb +48 -10
  204. data/lib/active_support/message_encryptor.rb +111 -37
  205. data/lib/active_support/message_verifier.rb +124 -21
  206. data/lib/active_support/messages/metadata.rb +80 -0
  207. data/lib/active_support/messages/rotation_configuration.rb +23 -0
  208. data/lib/active_support/messages/rotator.rb +57 -0
  209. data/lib/active_support/multibyte/chars.rb +19 -76
  210. data/lib/active_support/multibyte/unicode.rb +9 -331
  211. data/lib/active_support/multibyte.rb +3 -1
  212. data/lib/active_support/notifications/fanout.rb +165 -37
  213. data/lib/active_support/notifications/instrumenter.rb +92 -11
  214. data/lib/active_support/notifications.rb +96 -30
  215. data/lib/active_support/number_helper/number_converter.rb +8 -9
  216. data/lib/active_support/number_helper/number_to_currency_converter.rb +14 -12
  217. data/lib/active_support/number_helper/number_to_delimited_converter.rb +6 -3
  218. data/lib/active_support/number_helper/number_to_human_converter.rb +6 -3
  219. data/lib/active_support/number_helper/number_to_human_size_converter.rb +7 -4
  220. data/lib/active_support/number_helper/number_to_percentage_converter.rb +5 -1
  221. data/lib/active_support/number_helper/number_to_phone_converter.rb +6 -3
  222. data/lib/active_support/number_helper/number_to_rounded_converter.rb +14 -27
  223. data/lib/active_support/number_helper/rounding_helper.rb +16 -34
  224. data/lib/active_support/number_helper.rb +38 -12
  225. data/lib/active_support/option_merger.rb +19 -6
  226. data/lib/active_support/ordered_hash.rb +4 -2
  227. data/lib/active_support/ordered_options.rb +18 -6
  228. data/lib/active_support/parameter_filter.rb +138 -0
  229. data/lib/active_support/per_thread_registry.rb +8 -1
  230. data/lib/active_support/proxy_object.rb +2 -0
  231. data/lib/active_support/rails.rb +3 -10
  232. data/lib/active_support/railtie.rb +112 -11
  233. data/lib/active_support/reloader.rb +12 -11
  234. data/lib/active_support/rescuable.rb +19 -18
  235. data/lib/active_support/ruby_features.rb +7 -0
  236. data/lib/active_support/secure_compare_rotator.rb +51 -0
  237. data/lib/active_support/security_utils.rb +26 -15
  238. data/lib/active_support/string_inquirer.rb +4 -3
  239. data/lib/active_support/subscriber.rb +81 -42
  240. data/lib/active_support/tagged_logging.rb +45 -9
  241. data/lib/active_support/test_case.rb +86 -2
  242. data/lib/active_support/testing/assertions.rb +89 -21
  243. data/lib/active_support/testing/autorun.rb +2 -0
  244. data/lib/active_support/testing/constant_lookup.rb +2 -0
  245. data/lib/active_support/testing/declarative.rb +2 -0
  246. data/lib/active_support/testing/deprecation.rb +54 -2
  247. data/lib/active_support/testing/file_fixtures.rb +4 -0
  248. data/lib/active_support/testing/isolation.rb +6 -4
  249. data/lib/active_support/testing/method_call_assertions.rb +34 -5
  250. data/lib/active_support/testing/parallelization/server.rb +82 -0
  251. data/lib/active_support/testing/parallelization/worker.rb +103 -0
  252. data/lib/active_support/testing/parallelization.rb +55 -0
  253. data/lib/active_support/testing/parallelize_executor.rb +76 -0
  254. data/lib/active_support/testing/setup_and_teardown.rb +12 -7
  255. data/lib/active_support/testing/stream.rb +6 -7
  256. data/lib/active_support/testing/tagged_logging.rb +3 -1
  257. data/lib/active_support/testing/time_helpers.rb +91 -15
  258. data/lib/active_support/time.rb +2 -0
  259. data/lib/active_support/time_with_zone.rb +168 -56
  260. data/lib/active_support/values/time_zone.rb +85 -37
  261. data/lib/active_support/version.rb +3 -1
  262. data/lib/active_support/xml_mini/jdom.rb +6 -5
  263. data/lib/active_support/xml_mini/libxml.rb +9 -7
  264. data/lib/active_support/xml_mini/libxmlsax.rb +7 -5
  265. data/lib/active_support/xml_mini/nokogiri.rb +8 -6
  266. data/lib/active_support/xml_mini/nokogirisax.rb +6 -4
  267. data/lib/active_support/xml_mini/rexml.rb +13 -4
  268. data/lib/active_support/xml_mini.rb +10 -15
  269. data/lib/active_support.rb +30 -9
  270. metadata +76 -35
  271. data/lib/active_support/core_ext/array/prepend_and_append.rb +0 -7
  272. data/lib/active_support/core_ext/hash/compact.rb +0 -27
  273. data/lib/active_support/core_ext/hash/transform_values.rb +0 -30
  274. data/lib/active_support/core_ext/kernel/agnostics.rb +0 -11
  275. data/lib/active_support/core_ext/marshal.rb +0 -22
  276. data/lib/active_support/core_ext/module/reachable.rb +0 -8
  277. data/lib/active_support/core_ext/numeric/inquiry.rb +0 -26
  278. data/lib/active_support/core_ext/range/include_range.rb +0 -23
  279. data/lib/active_support/values/unicode_tables.dat +0 -0
@@ -1,7 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "openssl"
2
4
  require "base64"
3
- require "active_support/core_ext/array/extract_options"
5
+ require "active_support/core_ext/module/attribute_accessors"
4
6
  require "active_support/message_verifier"
7
+ require "active_support/messages/metadata"
5
8
 
6
9
  module ActiveSupport
7
10
  # MessageEncryptor is a simple way to encrypt values which get stored
@@ -10,18 +13,93 @@ module ActiveSupport
10
13
  # The cipher text and initialization vector are base64 encoded and returned
11
14
  # to you.
12
15
  #
13
- # This can be used in situations similar to the <tt>MessageVerifier</tt>, but
16
+ # This can be used in situations similar to the MessageVerifier, but
14
17
  # where you don't want users to be able to determine the value of the payload.
15
18
  #
16
- # salt = SecureRandom.random_bytes(64)
17
- # key = ActiveSupport::KeyGenerator.new('password').generate_key(salt, 32) # => "\x89\xE0\x156\xAC..."
18
- # crypt = ActiveSupport::MessageEncryptor.new(key) # => #<ActiveSupport::MessageEncryptor ...>
19
- # encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..."
20
- # crypt.decrypt_and_verify(encrypted_data) # => "my secret data"
19
+ # len = ActiveSupport::MessageEncryptor.key_len
20
+ # salt = SecureRandom.random_bytes(len)
21
+ # key = ActiveSupport::KeyGenerator.new('password').generate_key(salt, len) # => "\x89\xE0\x156\xAC..."
22
+ # crypt = ActiveSupport::MessageEncryptor.new(key) # => #<ActiveSupport::MessageEncryptor ...>
23
+ # encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..."
24
+ # crypt.decrypt_and_verify(encrypted_data) # => "my secret data"
25
+ #
26
+ # The +decrypt_and_verify+ method will raise an
27
+ # <tt>ActiveSupport::MessageEncryptor::InvalidMessage</tt> exception if the data
28
+ # provided cannot be decrypted or verified.
29
+ #
30
+ # crypt.decrypt_and_verify('not encrypted data') # => ActiveSupport::MessageEncryptor::InvalidMessage
31
+ #
32
+ # === Confining messages to a specific purpose
33
+ #
34
+ # By default any message can be used throughout your app. But they can also be
35
+ # confined to a specific +:purpose+.
36
+ #
37
+ # token = crypt.encrypt_and_sign("this is the chair", purpose: :login)
38
+ #
39
+ # Then that same purpose must be passed when verifying to get the data back out:
40
+ #
41
+ # crypt.decrypt_and_verify(token, purpose: :login) # => "this is the chair"
42
+ # crypt.decrypt_and_verify(token, purpose: :shipping) # => nil
43
+ # crypt.decrypt_and_verify(token) # => nil
44
+ #
45
+ # Likewise, if a message has no purpose it won't be returned when verifying with
46
+ # a specific purpose.
47
+ #
48
+ # token = crypt.encrypt_and_sign("the conversation is lively")
49
+ # crypt.decrypt_and_verify(token, purpose: :scare_tactics) # => nil
50
+ # crypt.decrypt_and_verify(token) # => "the conversation is lively"
51
+ #
52
+ # === Making messages expire
53
+ #
54
+ # By default messages last forever and verifying one year from now will still
55
+ # return the original value. But messages can be set to expire at a given
56
+ # time with +:expires_in+ or +:expires_at+.
57
+ #
58
+ # crypt.encrypt_and_sign(parcel, expires_in: 1.month)
59
+ # crypt.encrypt_and_sign(doowad, expires_at: Time.now.end_of_year)
60
+ #
61
+ # Then the messages can be verified and returned up to the expire time.
62
+ # Thereafter, verifying returns +nil+.
63
+ #
64
+ # === Rotating keys
65
+ #
66
+ # MessageEncryptor also supports rotating out old configurations by falling
67
+ # back to a stack of encryptors. Call +rotate+ to build and add an encryptor
68
+ # so +decrypt_and_verify+ will also try the fallback.
69
+ #
70
+ # By default any rotated encryptors use the values of the primary
71
+ # encryptor unless specified otherwise.
72
+ #
73
+ # You'd give your encryptor the new defaults:
74
+ #
75
+ # crypt = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm")
76
+ #
77
+ # Then gradually rotate the old values out by adding them as fallbacks. Any message
78
+ # generated with the old values will then work until the rotation is removed.
79
+ #
80
+ # crypt.rotate old_secret # Fallback to an old secret instead of @secret.
81
+ # crypt.rotate cipher: "aes-256-cbc" # Fallback to an old cipher instead of aes-256-gcm.
82
+ #
83
+ # Though if both the secret and the cipher was changed at the same time,
84
+ # the above should be combined into:
85
+ #
86
+ # crypt.rotate old_secret, cipher: "aes-256-cbc"
21
87
  class MessageEncryptor
22
- DEFAULT_CIPHER = "aes-256-cbc"
88
+ prepend Messages::Rotator::Encryptor
89
+
90
+ cattr_accessor :use_authenticated_message_encryption, instance_accessor: false, default: false
23
91
 
24
- module NullSerializer #:nodoc:
92
+ class << self
93
+ def default_cipher # :nodoc:
94
+ if use_authenticated_message_encryption
95
+ "aes-256-gcm"
96
+ else
97
+ "aes-256-cbc"
98
+ end
99
+ end
100
+ end
101
+
102
+ module NullSerializer # :nodoc:
25
103
  def self.load(value)
26
104
  value
27
105
  end
@@ -31,7 +109,7 @@ module ActiveSupport
31
109
  end
32
110
  end
33
111
 
34
- module NullVerifier #:nodoc:
112
+ module NullVerifier # :nodoc:
35
113
  def self.verify(value)
36
114
  value
37
115
  end
@@ -45,53 +123,50 @@ module ActiveSupport
45
123
  OpenSSLCipherError = OpenSSL::Cipher::CipherError
46
124
 
47
125
  # Initialize a new MessageEncryptor. +secret+ must be at least as long as
48
- # the cipher key size. For the default 'aes-256-cbc' cipher, this is 256
126
+ # the cipher key size. For the default 'aes-256-gcm' cipher, this is 256
49
127
  # bits. If you are using a user-entered secret, you can generate a suitable
50
- # key by using <tt>ActiveSupport::KeyGenerator</tt> or a similar key
128
+ # key by using ActiveSupport::KeyGenerator or a similar key
51
129
  # derivation function.
52
130
  #
53
- # First additional parameter is used as the signature key for +MessageVerifier+.
131
+ # First additional parameter is used as the signature key for MessageVerifier.
54
132
  # This allows you to specify keys to encrypt and sign data.
55
133
  #
56
134
  # ActiveSupport::MessageEncryptor.new('secret', 'signature_secret')
57
135
  #
58
136
  # Options:
59
137
  # * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by
60
- # <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-cbc'.
138
+ # <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-gcm'.
61
139
  # * <tt>:digest</tt> - String of digest to use for signing. Default is
62
140
  # +SHA1+. Ignored when using an AEAD cipher like 'aes-256-gcm'.
63
141
  # * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+.
64
- def initialize(secret, *signature_key_or_options)
65
- options = signature_key_or_options.extract_options!
66
- sign_secret = signature_key_or_options.first
142
+ def initialize(secret, sign_secret = nil, cipher: nil, digest: nil, serializer: nil)
67
143
  @secret = secret
68
144
  @sign_secret = sign_secret
69
- @cipher = options[:cipher] || DEFAULT_CIPHER
70
- @digest = options[:digest] || "SHA1" unless aead_mode?
145
+ @cipher = cipher || self.class.default_cipher
146
+ @digest = digest || "SHA1" unless aead_mode?
71
147
  @verifier = resolve_verifier
72
- @serializer = options[:serializer] || Marshal
148
+ @serializer = serializer || Marshal
73
149
  end
74
150
 
75
151
  # Encrypt and sign a message. We need to sign the message in order to avoid
76
- # padding attacks. Reference: http://www.limited-entropy.com/padding-oracle-attacks.
77
- def encrypt_and_sign(value)
78
- verifier.generate(_encrypt(value))
152
+ # 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))
79
155
  end
80
156
 
81
157
  # Decrypt and verify a message. We need to verify the message in order to
82
- # avoid padding attacks. Reference: http://www.limited-entropy.com/padding-oracle-attacks.
83
- def decrypt_and_verify(value)
84
- _decrypt(verifier.verify(value))
158
+ # 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)
85
161
  end
86
162
 
87
163
  # Given a cipher, returns the key length of the cipher to help generate the key of desired size
88
- def self.key_len(cipher = DEFAULT_CIPHER)
164
+ def self.key_len(cipher = default_cipher)
89
165
  OpenSSL::Cipher.new(cipher).key_len
90
166
  end
91
167
 
92
168
  private
93
-
94
- def _encrypt(value)
169
+ def _encrypt(value, **metadata_options)
95
170
  cipher = new_cipher
96
171
  cipher.encrypt
97
172
  cipher.key = @secret
@@ -100,17 +175,17 @@ module ActiveSupport
100
175
  iv = cipher.random_iv
101
176
  cipher.auth_data = "" if aead_mode?
102
177
 
103
- encrypted_data = cipher.update(@serializer.dump(value))
178
+ encrypted_data = cipher.update(Messages::Metadata.wrap(@serializer.dump(value), **metadata_options))
104
179
  encrypted_data << cipher.final
105
180
 
106
181
  blob = "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}"
107
- blob << "--#{::Base64.strict_encode64 cipher.auth_tag}" if aead_mode?
182
+ blob = "#{blob}--#{::Base64.strict_encode64 cipher.auth_tag}" if aead_mode?
108
183
  blob
109
184
  end
110
185
 
111
- def _decrypt(encrypted_message)
186
+ def _decrypt(encrypted_message, purpose)
112
187
  cipher = new_cipher
113
- encrypted_data, iv, auth_tag = encrypted_message.split("--".freeze).map { |v| ::Base64.strict_decode64(v) }
188
+ encrypted_data, iv, auth_tag = encrypted_message.split("--").map { |v| ::Base64.strict_decode64(v) }
114
189
 
115
190
  # Currently the OpenSSL bindings do not raise an error if auth_tag is
116
191
  # truncated, which would allow an attacker to easily forge it. See
@@ -128,7 +203,8 @@ module ActiveSupport
128
203
  decrypted_data = cipher.update(encrypted_data)
129
204
  decrypted_data << cipher.final
130
205
 
131
- @serializer.load(decrypted_data)
206
+ message = Messages::Metadata.verify(decrypted_data, purpose)
207
+ @serializer.load(message) if message
132
208
  rescue OpenSSLCipherError, TypeError, ArgumentError
133
209
  raise InvalidMessage
134
210
  end
@@ -137,9 +213,7 @@ module ActiveSupport
137
213
  OpenSSL::Cipher.new(@cipher)
138
214
  end
139
215
 
140
- def verifier
141
- @verifier
142
- end
216
+ attr_reader :verifier
143
217
 
144
218
  def aead_mode?
145
219
  @aead_mode ||= new_cipher.authenticated?
@@ -1,6 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
1
4
  require "base64"
2
5
  require "active_support/core_ext/object/blank"
3
6
  require "active_support/security_utils"
7
+ require "active_support/messages/metadata"
8
+ require "active_support/messages/rotator"
4
9
 
5
10
  module ActiveSupport
6
11
  # +MessageVerifier+ makes it easy to generate and verify messages which are
@@ -27,17 +32,86 @@ module ActiveSupport
27
32
  #
28
33
  # +MessageVerifier+ creates HMAC signatures using SHA1 hash algorithm by default.
29
34
  # If you want to use a different hash algorithm, you can change it by providing
30
- # `:digest` key as an option while initializing the verifier:
35
+ # +:digest+ key as an option while initializing the verifier:
31
36
  #
32
37
  # @verifier = ActiveSupport::MessageVerifier.new('s3Krit', digest: 'SHA256')
38
+ #
39
+ # === Confining messages to a specific purpose
40
+ #
41
+ # By default any message can be used throughout your app. But they can also be
42
+ # confined to a specific +:purpose+.
43
+ #
44
+ # token = @verifier.generate("this is the chair", purpose: :login)
45
+ #
46
+ # Then that same purpose must be passed when verifying to get the data back out:
47
+ #
48
+ # @verifier.verified(token, purpose: :login) # => "this is the chair"
49
+ # @verifier.verified(token, purpose: :shipping) # => nil
50
+ # @verifier.verified(token) # => nil
51
+ #
52
+ # @verifier.verify(token, purpose: :login) # => "this is the chair"
53
+ # @verifier.verify(token, purpose: :shipping) # => ActiveSupport::MessageVerifier::InvalidSignature
54
+ # @verifier.verify(token) # => ActiveSupport::MessageVerifier::InvalidSignature
55
+ #
56
+ # Likewise, if a message has no purpose it won't be returned when verifying with
57
+ # a specific purpose.
58
+ #
59
+ # token = @verifier.generate("the conversation is lively")
60
+ # @verifier.verified(token, purpose: :scare_tactics) # => nil
61
+ # @verifier.verified(token) # => "the conversation is lively"
62
+ #
63
+ # @verifier.verify(token, purpose: :scare_tactics) # => ActiveSupport::MessageVerifier::InvalidSignature
64
+ # @verifier.verify(token) # => "the conversation is lively"
65
+ #
66
+ # === Making messages expire
67
+ #
68
+ # By default messages last forever and verifying one year from now will still
69
+ # return the original value. But messages can be set to expire at a given
70
+ # time with +:expires_in+ or +:expires_at+.
71
+ #
72
+ # @verifier.generate("parcel", expires_in: 1.month)
73
+ # @verifier.generate("doowad", expires_at: Time.now.end_of_year)
74
+ #
75
+ # Then the messages can be verified and returned up to the expire time.
76
+ # Thereafter, the +verified+ method returns +nil+ while +verify+ raises
77
+ # <tt>ActiveSupport::MessageVerifier::InvalidSignature</tt>.
78
+ #
79
+ # === Rotating keys
80
+ #
81
+ # MessageVerifier also supports rotating out old configurations by falling
82
+ # back to a stack of verifiers. Call +rotate+ to build and add a verifier so
83
+ # either +verified+ or +verify+ will also try verifying with the fallback.
84
+ #
85
+ # By default any rotated verifiers use the values of the primary
86
+ # verifier unless specified otherwise.
87
+ #
88
+ # You'd give your verifier the new defaults:
89
+ #
90
+ # verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA512", serializer: JSON)
91
+ #
92
+ # Then gradually rotate the old values out by adding them as fallbacks. Any message
93
+ # generated with the old values will then work until the rotation is removed.
94
+ #
95
+ # verifier.rotate old_secret # Fallback to an old secret instead of @secret.
96
+ # verifier.rotate digest: "SHA256" # Fallback to an old digest instead of SHA512.
97
+ # verifier.rotate serializer: Marshal # Fallback to an old serializer instead of JSON.
98
+ #
99
+ # Though the above would most likely be combined into one rotation:
100
+ #
101
+ # verifier.rotate old_secret, digest: "SHA256", serializer: Marshal
33
102
  class MessageVerifier
103
+ prepend Messages::Rotator::Verifier
104
+
34
105
  class InvalidSignature < StandardError; end
35
106
 
36
- def initialize(secret, options = {})
107
+ SEPARATOR = "--" # :nodoc:
108
+ SEPARATOR_LENGTH = SEPARATOR.length # :nodoc:
109
+
110
+ def initialize(secret, digest: nil, serializer: nil)
37
111
  raise ArgumentError, "Secret should not be nil." unless secret
38
112
  @secret = secret
39
- @digest = options[:digest] || "SHA1"
40
- @serializer = options[:serializer] || Marshal
113
+ @digest = digest&.to_s || "SHA1"
114
+ @serializer = serializer || Marshal
41
115
  end
42
116
 
43
117
  # Checks if a signed message could have been generated by signing an object
@@ -50,10 +124,8 @@ module ActiveSupport
50
124
  # tampered_message = signed_message.chop # editing the message invalidates the signature
51
125
  # verifier.valid_message?(tampered_message) # => false
52
126
  def valid_message?(signed_message)
53
- return if signed_message.nil? || !signed_message.valid_encoding? || signed_message.blank?
54
-
55
- data, digest = signed_message.split("--".freeze)
56
- data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data))
127
+ data, digest = get_data_and_digest_from(signed_message)
128
+ digest_matches_data?(digest, data)
57
129
  end
58
130
 
59
131
  # Decodes the signed message using the +MessageVerifier+'s secret.
@@ -77,11 +149,12 @@ module ActiveSupport
77
149
  #
78
150
  # incompatible_message = "test--dad7b06c94abba8d46a15fafaef56c327665d5ff"
79
151
  # verifier.verified(incompatible_message) # => TypeError: incompatible marshal file format
80
- def verified(signed_message)
81
- if valid_message?(signed_message)
152
+ def verified(signed_message, purpose: nil, **)
153
+ data, digest = get_data_and_digest_from(signed_message)
154
+ if digest_matches_data?(digest, data)
82
155
  begin
83
- data = signed_message.split("--".freeze)[0]
84
- @serializer.load(decode(data))
156
+ message = Messages::Metadata.verify(decode(data), purpose)
157
+ @serializer.load(message) if message
85
158
  rescue ArgumentError => argument_error
86
159
  return if argument_error.message.include?("invalid base64")
87
160
  raise
@@ -101,20 +174,20 @@ module ActiveSupport
101
174
  #
102
175
  # other_verifier = ActiveSupport::MessageVerifier.new 'd1ff3r3nt-s3Krit'
103
176
  # other_verifier.verify(signed_message) # => ActiveSupport::MessageVerifier::InvalidSignature
104
- def verify(signed_message)
105
- verified(signed_message) || raise(InvalidSignature)
177
+ def verify(*args, **options)
178
+ verified(*args, **options) || raise(InvalidSignature)
106
179
  end
107
180
 
108
181
  # Generates a signed message for the provided value.
109
182
  #
110
- # The message is signed with the +MessageVerifier+'s secret. Without knowing
111
- # the secret, the original value cannot be extracted from the message.
183
+ # The message is signed with the +MessageVerifier+'s secret.
184
+ # Returns Base64-encoded message joined with the generated signature.
112
185
  #
113
186
  # verifier = ActiveSupport::MessageVerifier.new 's3Krit'
114
187
  # verifier.generate 'a private message' # => "BAhJIhRwcml2YXRlLW1lc3NhZ2UGOgZFVA==--e2d724331ebdee96a10fb99b089508d1c72bd772"
115
- def generate(value)
116
- data = encode(@serializer.dump(value))
117
- "#{data}--#{generate_digest(data)}"
188
+ def generate(value, expires_at: nil, expires_in: nil, purpose: nil)
189
+ data = encode(Messages::Metadata.wrap(@serializer.dump(value), expires_at: expires_at, expires_in: expires_in, purpose: purpose))
190
+ "#{data}#{SEPARATOR}#{generate_digest(data)}"
118
191
  end
119
192
 
120
193
  private
@@ -127,8 +200,38 @@ module ActiveSupport
127
200
  end
128
201
 
129
202
  def generate_digest(data)
130
- require "openssl" unless defined?(OpenSSL)
131
- OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@digest).new, @secret, data)
203
+ OpenSSL::HMAC.hexdigest(@digest, @secret, data)
204
+ end
205
+
206
+ def digest_length_in_hex
207
+ # In hexadecimal (AKA base16) it takes 4 bits to represent a character,
208
+ # hence we multiply the digest's length (in bytes) by 8 to get it in
209
+ # bits and divide by 4 to get its number of characters it hex. Well, 8
210
+ # divided by 4 is 2.
211
+ @digest_length_in_hex ||= OpenSSL::Digest.new(@digest).digest_length * 2
212
+ end
213
+
214
+ def separator_index_for(signed_message)
215
+ index = signed_message.length - digest_length_in_hex - SEPARATOR_LENGTH
216
+ return if index.negative? || signed_message[index, SEPARATOR_LENGTH] != SEPARATOR
217
+
218
+ index
219
+ end
220
+
221
+ def get_data_and_digest_from(signed_message)
222
+ return if signed_message.nil? || !signed_message.valid_encoding? || signed_message.empty?
223
+
224
+ separator_index = separator_index_for(signed_message)
225
+ return if separator_index.nil?
226
+
227
+ data = signed_message[0...separator_index]
228
+ digest = signed_message[separator_index + SEPARATOR_LENGTH..-1]
229
+
230
+ [data, digest]
231
+ end
232
+
233
+ def digest_matches_data?(digest, data)
234
+ data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data))
132
235
  end
133
236
  end
134
237
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module ActiveSupport
6
+ module Messages # :nodoc:
7
+ class Metadata # :nodoc:
8
+ def initialize(message, expires_at = nil, purpose = nil)
9
+ @message, @purpose = message, purpose
10
+ @expires_at = expires_at.is_a?(String) ? parse_expires_at(expires_at) : expires_at
11
+ end
12
+
13
+ def as_json(options = {})
14
+ { _rails: { message: @message, exp: @expires_at, pur: @purpose } }
15
+ end
16
+
17
+ class << self
18
+ def wrap(message, expires_at: nil, expires_in: nil, purpose: nil)
19
+ if expires_at || expires_in || purpose
20
+ JSON.encode new(encode(message), pick_expiry(expires_at, expires_in), purpose)
21
+ else
22
+ message
23
+ end
24
+ end
25
+
26
+ def verify(message, purpose)
27
+ extract_metadata(message).verify(purpose)
28
+ end
29
+
30
+ private
31
+ def pick_expiry(expires_at, expires_in)
32
+ if expires_at
33
+ expires_at.utc.iso8601(3)
34
+ elsif expires_in
35
+ Time.now.utc.advance(seconds: expires_in).iso8601(3)
36
+ end
37
+ end
38
+
39
+ def extract_metadata(message)
40
+ data = JSON.decode(message) rescue nil
41
+
42
+ if data.is_a?(Hash) && data.key?("_rails")
43
+ new(decode(data["_rails"]["message"]), data["_rails"]["exp"], data["_rails"]["pur"])
44
+ else
45
+ new(message)
46
+ end
47
+ end
48
+
49
+ def encode(message)
50
+ ::Base64.strict_encode64(message)
51
+ end
52
+
53
+ def decode(message)
54
+ ::Base64.strict_decode64(message)
55
+ end
56
+ end
57
+
58
+ def verify(purpose)
59
+ @message if match?(purpose) && fresh?
60
+ end
61
+
62
+ private
63
+ def match?(purpose)
64
+ @purpose.to_s == purpose.to_s
65
+ end
66
+
67
+ def fresh?
68
+ @expires_at.nil? || Time.now.utc < @expires_at
69
+ end
70
+
71
+ def parse_expires_at(expires_at)
72
+ if ActiveSupport.use_standard_json_time_format
73
+ Time.iso8601(expires_at)
74
+ else
75
+ Time.parse(expires_at)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport
4
+ module Messages
5
+ class RotationConfiguration # :nodoc:
6
+ attr_reader :signed, :encrypted
7
+
8
+ def initialize
9
+ @signed, @encrypted = [], []
10
+ end
11
+
12
+ def rotate(kind, *args, **options)
13
+ args << options unless options.empty?
14
+ case kind
15
+ when :signed
16
+ @signed << args
17
+ when :encrypted
18
+ @encrypted << args
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport
4
+ module Messages
5
+ module Rotator # :nodoc:
6
+ def initialize(*secrets, on_rotation: nil, **options)
7
+ super(*secrets, **options)
8
+
9
+ @options = options
10
+ @rotations = []
11
+ @on_rotation = on_rotation
12
+ end
13
+
14
+ def rotate(*secrets, **options)
15
+ @rotations << build_rotation(*secrets, @options.merge(options))
16
+ end
17
+
18
+ module Encryptor
19
+ include Rotator
20
+
21
+ def decrypt_and_verify(*args, on_rotation: @on_rotation, **options)
22
+ super
23
+ rescue MessageEncryptor::InvalidMessage, MessageVerifier::InvalidSignature
24
+ run_rotations(on_rotation) { |encryptor| encryptor.decrypt_and_verify(*args, **options) } || raise
25
+ end
26
+
27
+ private
28
+ def build_rotation(secret = @secret, sign_secret = @sign_secret, options)
29
+ self.class.new(secret, sign_secret, **options)
30
+ end
31
+ end
32
+
33
+ module Verifier
34
+ include Rotator
35
+
36
+ def verified(*args, on_rotation: @on_rotation, **options)
37
+ super || run_rotations(on_rotation) { |verifier| verifier.verified(*args, **options) }
38
+ end
39
+
40
+ private
41
+ def build_rotation(secret = @secret, options)
42
+ self.class.new(secret, **options)
43
+ end
44
+ end
45
+
46
+ private
47
+ def run_rotations(on_rotation)
48
+ @rotations.find do |rotation|
49
+ if message = yield(rotation) rescue next
50
+ on_rotation&.call
51
+ return message
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end