7ee34d9efb
4 Commits
Author | SHA1 | Message | Date | |
---|---|---|---|---|
Jonathan Hefner
|
d3917f5fdd |
Use throw for message error handling control flow
There are multiple points of failure when processing a message with `MessageEncryptor` or `MessageVerifier`, and there several ways we might want to handle those failures. For example, swallowing a failure with `MessageVerifier#verified`, or raising a specific exception with `MessageVerifier#verify`, or conditionally ignoring a failure when rotations are configured. Prior to this commit, the _internal_ logic of handling failures was implemented using a mix of `nil` return values and raised exceptions. This commit reimplements the internal logic using `throw` and a few precisely targeted `rescue`s. This accomplishes several things: * Allow rotation of serializers for `MessageVerifier`. Previously, errors from a `MessageVerifier`'s initial serializer were never rescued. Thus, the serializer could not be rotated: ```ruby old_verifier = ActiveSupport::MessageVerifier.new("secret", serializer: Marshal) new_verifier = ActiveSupport::MessageVerifier.new("secret", serializer: JSON) new_verifier.rotate(serializer: Marshal) message = old_verifier.generate("message") new_verifier.verify(message) # BEFORE: # => raises JSON::ParserError # AFTER: # => "message" ``` * Allow rotation of serializers for `MessageEncryptor` when using a non-standard initial serializer. Similar to `MessageVerifier`, the serializer could not be rotated when the initial serializer raised an error other than `TypeError` or `JSON::ParserError`, such as `Psych::SyntaxError` or a custom error. * Raise `MessageEncryptor::InvalidMessage` from `decrypt_and_verify` regardless of cipher. Previously, when a `MessageEncryptor` was using a non-AEAD cipher such as AES-256-CBC, a corrupt or tampered message would raise `MessageVerifier::InvalidSignature` due to reliance on `MessageVerifier` for verification. Now, the verification mechanism is transparent to the user: ```ruby encryptor = ActiveSupport::MessageEncryptor.new("x" * 32, cipher: "aes-256-gcm") message = encryptor.encrypt_and_sign("message") encryptor.decrypt_and_verify(message.next) # => raises ActiveSupport::MessageEncryptor::InvalidMessage encryptor = ActiveSupport::MessageEncryptor.new("x" * 32, cipher: "aes-256-cbc") message = encryptor.encrypt_and_sign("message") encryptor.decrypt_and_verify(message.next) # BEFORE: # => raises ActiveSupport::MessageVerifier::InvalidSignature # AFTER: # => raises ActiveSupport::MessageEncryptor::InvalidMessage ``` * Support `nil` original value when using `MessageVerifier#verify`. Previously, `MessageVerifier#verify` did not work with `nil` original values, though both `MessageVerifier#verified` and `MessageEncryptor#decrypt_and_verify` do: ```ruby encryptor = ActiveSupport::MessageEncryptor.new("x" * 32) message = encryptor.encrypt_and_sign(nil) encryptor.decrypt_and_verify(message) # => nil verifier = ActiveSupport::MessageVerifier.new("secret") message = verifier.generate(nil) verifier.verified(message) # => nil verifier.verify(message) # BEFORE: # => raises ActiveSupport::MessageVerifier::InvalidSignature # AFTER: # => nil ``` * Improve performance of verifying a message when it has expired and one or more rotations have been configured: ```ruby # frozen_string_literal: true require "benchmark/ips" require "active_support/all" verifier = ActiveSupport::MessageVerifier.new("new secret") verifier.rotate("old secret") message = verifier.generate({ "data" => "x" * 100 }, expires_at: 1.day.ago) Benchmark.ips do |x| x.report("expired message") do verifier.verified(message) end end ``` __Before__ ``` Warming up -------------------------------------- expired message 1.442k i/100ms Calculating ------------------------------------- expired message 14.403k (± 1.7%) i/s - 72.100k in 5.007382s ``` __After__ ``` Warming up -------------------------------------- expired message 1.995k i/100ms Calculating ------------------------------------- expired message 19.992k (± 2.0%) i/s - 101.745k in 5.091421s ``` Fixes #47185. |
||
Jonathan Hefner
|
61f711a1ff |
Support Message{Encryptors,Verifiers}#rotate block
This commit adds a block form of `ActiveSupport::MessageEncryptors#rotate` and `ActiveSupport::MessageVerifiers#rotate`, which supports fine-grained per-salt rotation options. The block will receive a salt, and should return an appropriate options Hash. The block may also return `nil` to indicate that the rotation does not apply to the given salt. For example: ```ruby verifiers = ActiveSupport::MessageVerifiers.new { ... } verifiers.rotate(serializer: JSON, url_safe: true) verifiers.rotate do |salt| case salt when :foo { serializer: Marshal, url_safe: true } when :bar { serializer: Marshal, url_safe: false } end end # Uses `serializer: JSON, url_safe: true`. # Falls back to `serializer: Marshal, url_safe: true`. verifiers[:foo] # Uses `serializer: JSON, url_safe: true`. # Falls back to `serializer: Marshal, url_safe: false`. verifiers[:bar] # Uses `serializer: JSON, url_safe: true`. verifiers[:baz] ``` This can be particularly useful when migrating older configurations to a unified configuration. |
||
Jonathan Hefner
|
32ea85f1c2 |
Fix Active Support isolated build
Follow-up to #44179. Example failure: https://buildkite.com/rails/rails/builds/89840#018377c0-87f6-4d46-a0d9-4add41c4a798/1046-2214 |
||
Jonathan Hefner
|
baade55c69 |
Add Rails.application.message_verifiers
Currently, `Rails.application.message_verifier(name)` returns a `MessageVerifier` instance using a secret derived from the given `name`. Instances created this way always generate messages using the default options for `MessageVerifier.new`. Also, if there are older options to rotate for message verification, each instance created this way must be configured individually. In cases where Rails itself uses `Rails.application.message_verifier` (e.g. in Active Storage), discovering the name of the instance may require digging through Rails' source code. To alleviate these issues, this commit adds a `MessageVerifiers` factory class, which acts as a central point for configuring and creating `MessageVerifier` instances. Options passed to `MessageVerifiers#rotate` will be applied to `MessageVerifier` instances that `MessageVerifiers#[]` creates. So the following: ```ruby foo_verifier = Rails.application.message_verifier(:foo) foo_verifier.rotate(old_options) bar_verifier = Rails.application.message_verifier(:bar) bar_verifier.rotate(old_options) ``` Can be rewritten as: ```ruby Rails.application.message_verifiers.rotate(old_options) foo_verifier = Rails.application.message_verifiers[:foo] bar_verifier = Rails.application.message_verifiers[:bar] ``` Additionally, `Rails.application.message_verifiers` supports a `:secret_key_base` option, so old `secret_key_base` values can be rotated: ```ruby Rails.application.message_verifiers.rotate(secret_key_base: "old secret_key_base") ``` `MessageVerifiers` memoizes `MessageVerifier` instances. This also allows `MessageVerifier` instances to be overridden entirely: ```ruby Rails.application.message_verifiers[:foo] = ActiveSupport::MessageVerifier.new(other_secret) ``` For parity, this commit also adds a `MessageEncryptors` factory class, which fulfills the same role for `MessageEncryptor` instances. |