d3917f5fdd
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.
55 lines
2.1 KiB
Ruby
55 lines
2.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_relative "abstract_unit"
|
|
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
|
|
|
|
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))
|
|
rescue ActiveSupport::MessageEncryptor::InvalidMessage
|
|
nil
|
|
end
|
|
end
|