activesupport 4.0.12 → 7.0.2.4

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