activesupport 5.0.0 → 6.1.0

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