rails/activesupport/test/message_encryptors_test.rb

55 lines
2.1 KiB
Ruby
Raw Normal View History

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.
2022-01-15 20:07:13 +00:00
# frozen_string_literal: true
require_relative "abstract_unit"
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.
2022-01-15 20:07:13 +00:00
require_relative "rotation_coordinator_tests"
class MessageEncryptorsTest < ActiveSupport::TestCase
include RotationCoordinatorTests
test "can override secret generator" do
secret_generator = ->(salt, secret_length:) { salt[0] * secret_length }
coordinator = make_coordinator.rotate(secret_generator: secret_generator)
assert_equal "message", roundtrip("message", coordinator["salt"])
assert_nil roundtrip("message", @coordinator["salt"], coordinator["salt"])
end
test "supports arbitrary secret generator kwargs" do
secret_generator = ->(salt, secret_length:, foo:, bar: nil) { foo[bar] * secret_length }
coordinator = ActiveSupport::MessageEncryptors.new(&secret_generator)
coordinator.rotate(foo: "foo", bar: 0)
assert_equal "message", roundtrip("message", coordinator["salt"])
end
test "supports arbitrary secret generator kwargs when using #rotate block" do
secret_generator = ->(salt, secret_length:, foo:, bar: nil) { foo[bar] * secret_length }
coordinator = ActiveSupport::MessageEncryptors.new(&secret_generator)
coordinator.rotate { { foo: "foo", bar: 0 } }
assert_equal "message", roundtrip("message", coordinator["salt"])
end
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.
2022-01-15 20:07:13 +00:00
test "supports separate secrets for encryption and signing" do
secret_generator = proc { |*args, **kwargs| [SECRET_GENERATOR.call(*args, **kwargs), "signing secret"] }
coordinator = ActiveSupport::MessageEncryptors.new(&secret_generator)
coordinator.rotate_defaults
assert_equal "message", roundtrip("message", coordinator["salt"])
assert_nil roundtrip("message", @coordinator["salt"], coordinator["salt"])
end
private
SECRET_GENERATOR = proc { |salt, secret_length:| "".ljust(secret_length, salt) }
def make_coordinator
ActiveSupport::MessageEncryptors.new(&SECRET_GENERATOR)
end
def roundtrip(message, encryptor, decryptor = encryptor)
decryptor.decrypt_and_verify(encryptor.encrypt_and_sign(message))
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.
2023-02-07 19:56:59 +00:00
rescue ActiveSupport::MessageEncryptor::InvalidMessage
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.
2022-01-15 20:07:13 +00:00
nil
end
end