提交 57585b6f 编写于 作者: K Kasper Timm Hansen 提交者: GitHub

Merge pull request #30171 from kaspth/verifier-encryptor-null-serializer-metadata

Perform self-serialization once metadata is involved.
...@@ -121,14 +121,13 @@ def initialize(secret, *signature_key_or_options) ...@@ -121,14 +121,13 @@ def initialize(secret, *signature_key_or_options)
# Encrypt and sign a message. We need to sign the message in order to avoid # Encrypt and sign a message. We need to sign the message in order to avoid
# padding attacks. Reference: http://www.limited-entropy.com/padding-oracle-attacks. # padding attacks. Reference: http://www.limited-entropy.com/padding-oracle-attacks.
def encrypt_and_sign(value, expires_at: nil, expires_in: nil, purpose: nil) def encrypt_and_sign(value, expires_at: nil, expires_in: nil, purpose: nil)
data = Messages::Metadata.wrap(value, expires_at: expires_at, expires_in: expires_in, purpose: purpose) verifier.generate(_encrypt(value, expires_at: expires_at, expires_in: expires_in, purpose: purpose))
verifier.generate(_encrypt(data))
end end
# Decrypt and verify a message. We need to verify the message in order to # Decrypt and verify a message. We need to verify the message in order to
# avoid padding attacks. Reference: http://www.limited-entropy.com/padding-oracle-attacks. # avoid padding attacks. Reference: http://www.limited-entropy.com/padding-oracle-attacks.
def decrypt_and_verify(data, purpose: nil) def decrypt_and_verify(data, purpose: nil)
Messages::Metadata.verify(_decrypt(verifier.verify(data)), purpose) _decrypt(verifier.verify(data), purpose)
end end
# Given a cipher, returns the key length of the cipher to help generate the key of desired size # Given a cipher, returns the key length of the cipher to help generate the key of desired size
...@@ -137,7 +136,7 @@ def self.key_len(cipher = default_cipher) ...@@ -137,7 +136,7 @@ def self.key_len(cipher = default_cipher)
end end
private private
def _encrypt(value) def _encrypt(value, **metadata_options)
cipher = new_cipher cipher = new_cipher
cipher.encrypt cipher.encrypt
cipher.key = @secret cipher.key = @secret
...@@ -146,7 +145,7 @@ def _encrypt(value) ...@@ -146,7 +145,7 @@ def _encrypt(value)
iv = cipher.random_iv iv = cipher.random_iv
cipher.auth_data = "" if aead_mode? cipher.auth_data = "" if aead_mode?
encrypted_data = cipher.update(@serializer.dump(value)) encrypted_data = cipher.update(Messages::Metadata.wrap(@serializer.dump(value), metadata_options))
encrypted_data << cipher.final encrypted_data << cipher.final
blob = "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}" blob = "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}"
...@@ -154,7 +153,7 @@ def _encrypt(value) ...@@ -154,7 +153,7 @@ def _encrypt(value)
blob blob
end end
def _decrypt(encrypted_message) def _decrypt(encrypted_message, purpose)
cipher = new_cipher cipher = new_cipher
encrypted_data, iv, auth_tag = encrypted_message.split("--".freeze).map { |v| ::Base64.strict_decode64(v) } encrypted_data, iv, auth_tag = encrypted_message.split("--".freeze).map { |v| ::Base64.strict_decode64(v) }
...@@ -174,7 +173,8 @@ def _decrypt(encrypted_message) ...@@ -174,7 +173,8 @@ def _decrypt(encrypted_message)
decrypted_data = cipher.update(encrypted_data) decrypted_data = cipher.update(encrypted_data)
decrypted_data << cipher.final decrypted_data << cipher.final
@serializer.load(decrypted_data) message = Messages::Metadata.verify(decrypted_data, purpose)
@serializer.load(message) if message
rescue OpenSSLCipherError, TypeError, ArgumentError rescue OpenSSLCipherError, TypeError, ArgumentError
raise InvalidMessage raise InvalidMessage
end end
......
...@@ -124,7 +124,8 @@ def verified(signed_message, purpose: nil) ...@@ -124,7 +124,8 @@ def verified(signed_message, purpose: nil)
if valid_message?(signed_message) if valid_message?(signed_message)
begin begin
data = signed_message.split("--".freeze)[0] data = signed_message.split("--".freeze)[0]
Messages::Metadata.verify(@serializer.load(decode(data)), purpose) message = Messages::Metadata.verify(decode(data), purpose)
@serializer.load(message) if message
rescue ArgumentError => argument_error rescue ArgumentError => argument_error
return if argument_error.message.include?("invalid base64") return if argument_error.message.include?("invalid base64")
raise raise
...@@ -156,7 +157,7 @@ def verify(signed_message, purpose: nil) ...@@ -156,7 +157,7 @@ def verify(signed_message, purpose: nil)
# verifier = ActiveSupport::MessageVerifier.new 's3Krit' # verifier = ActiveSupport::MessageVerifier.new 's3Krit'
# verifier.generate 'a private message' # => "BAhJIhRwcml2YXRlLW1lc3NhZ2UGOgZFVA==--e2d724331ebdee96a10fb99b089508d1c72bd772" # verifier.generate 'a private message' # => "BAhJIhRwcml2YXRlLW1lc3NhZ2UGOgZFVA==--e2d724331ebdee96a10fb99b089508d1c72bd772"
def generate(value, expires_at: nil, expires_in: nil, purpose: nil) def generate(value, expires_at: nil, expires_in: nil, purpose: nil)
data = encode(@serializer.dump(Messages::Metadata.wrap(value, expires_at: expires_at, expires_in: expires_in, purpose: purpose))) data = encode(Messages::Metadata.wrap(@serializer.dump(value), expires_at: expires_at, expires_in: expires_in, purpose: purpose))
"#{data}--#{generate_digest(data)}" "#{data}--#{generate_digest(data)}"
end end
......
...@@ -5,27 +5,25 @@ ...@@ -5,27 +5,25 @@
module ActiveSupport module ActiveSupport
module Messages #:nodoc: module Messages #:nodoc:
class Metadata #:nodoc: class Metadata #:nodoc:
def initialize(expires_at, purpose) def initialize(message, expires_at = nil, purpose = nil)
@expires_at, @purpose = expires_at, purpose.to_s @message, @expires_at, @purpose = message, expires_at, purpose
end
def as_json(options = {})
{ _rails: { message: @message, exp: @expires_at, pur: @purpose } }
end end
class << self class << self
def wrap(message, expires_at: nil, expires_in: nil, purpose: nil) def wrap(message, expires_at: nil, expires_in: nil, purpose: nil)
if expires_at || expires_in || purpose if expires_at || expires_in || purpose
{ "value" => message, "_rails" => { "exp" => pick_expiry(expires_at, expires_in), "pur" => purpose } } JSON.encode new(encode(message), pick_expiry(expires_at, expires_in), purpose)
else else
message message
end end
end end
def verify(message, purpose) def verify(message, purpose)
metadata = extract_metadata(message) extract_metadata(message).verify(purpose)
if metadata.nil?
message if purpose.nil?
elsif metadata.match?(purpose) && metadata.fresh?
message["value"]
end
end end
private private
...@@ -38,19 +36,36 @@ def pick_expiry(expires_at, expires_in) ...@@ -38,19 +36,36 @@ def pick_expiry(expires_at, expires_in)
end end
def extract_metadata(message) def extract_metadata(message)
if message.is_a?(Hash) && message.key?("_rails") data = JSON.decode(message) rescue nil
new(message["_rails"]["exp"], message["_rails"]["pur"])
if data.is_a?(Hash) && data.key?("_rails")
new(decode(data["_rails"]["message"]), data["_rails"]["exp"], data["_rails"]["pur"])
else
new(message)
end end
end end
end
def match?(purpose) def encode(message)
@purpose == purpose.to_s ::Base64.strict_encode64(message)
end
def decode(message)
::Base64.strict_decode64(message)
end
end end
def fresh? def verify(purpose)
@expires_at.nil? || Time.now.utc < Time.iso8601(@expires_at) @message if match?(purpose) && fresh?
end end
private
def match?(purpose)
@purpose.to_s == purpose.to_s
end
def fresh?
@expires_at.nil? || Time.now.utc < Time.iso8601(@expires_at)
end
end end
end end
end end
...@@ -101,12 +101,12 @@ class MessageVerifierMetadataTest < ActiveSupport::TestCase ...@@ -101,12 +101,12 @@ class MessageVerifierMetadataTest < ActiveSupport::TestCase
def test_verify_raises_when_purpose_differs def test_verify_raises_when_purpose_differs
assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do
@verifier.verify(@verifier.generate(@message, purpose: "payment"), purpose: "shipping") @verifier.verify(generate(data, purpose: "payment"), purpose: "shipping")
end end
end end
def test_verify_raises_when_expired def test_verify_raises_when_expired
signed_message = @verifier.generate(@message, expires_in: 1.month) signed_message = generate(data, expires_in: 1.month)
travel 2.months travel 2.months
assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do
...@@ -141,3 +141,18 @@ def verifier_options ...@@ -141,3 +141,18 @@ def verifier_options
{ serializer: MessageVerifierTest::JSONSerializer.new } { serializer: MessageVerifierTest::JSONSerializer.new }
end end
end end
class MessageEncryptorMetadataNullSerializerTest < MessageVerifierMetadataTest
private
def data
"string message"
end
def null_serializing?
true
end
def verifier_options
{ serializer: ActiveSupport::MessageEncryptor::NullSerializer }
end
end
# frozen_string_literal: true # frozen_string_literal: true
module SharedMessageMetadataTests module SharedMessageMetadataTests
def setup
@message = { "credit_card_no" => "5012-6784-9087-5678", "card_holder" => { "name" => "Donald" } }
super
end
def teardown def teardown
travel_back travel_back
super super
end end
def null_serializing?
false
end
def test_encryption_and_decryption_with_same_purpose def test_encryption_and_decryption_with_same_purpose
assert_equal @message, parse(generate(@message, purpose: "checkout"), purpose: "checkout") assert_equal data, parse(generate(data, purpose: "checkout"), purpose: "checkout")
assert_equal @message, parse(generate(@message)) assert_equal data, parse(generate(data))
string_message = "address: #23, main street" string_message = "address: #23, main street"
assert_equal string_message, parse(generate(string_message, purpose: "shipping"), purpose: "shipping") assert_equal string_message, parse(generate(string_message, purpose: "shipping"), purpose: "shipping")
end
array_message = ["credit_card_no: 5012-6748-9087-5678", { "card_holder" => "Donald", "issued_on" => Time.local(2017) }, 12345] def test_verifies_array_when_purpose_matches
assert_equal array_message, parse(generate(array_message, purpose: "registration"), purpose: "registration") unless null_serializing?
data = [ "credit_card_no: 5012-6748-9087-5678", { "card_holder" => "Donald", "issued_on" => Time.local(2017) }, 12345 ]
assert_equal data, parse(generate(data, purpose: :registration), purpose: :registration)
end
end end
def test_encryption_and_decryption_with_different_purposes_returns_nil def test_encryption_and_decryption_with_different_purposes_returns_nil
assert_nil parse(generate(@message, purpose: "payment"), purpose: "sign up") assert_nil parse(generate(data, purpose: "payment"), purpose: "sign up")
assert_nil parse(generate(@message, purpose: "payment")) assert_nil parse(generate(data, purpose: "payment"))
assert_nil parse(generate(@message), purpose: "sign up") assert_nil parse(generate(data), purpose: "sign up")
assert_nil parse(generate(@message), purpose: "")
end end
def test_purpose_using_symbols def test_purpose_using_symbols
assert_equal @message, parse(generate(@message, purpose: :checkout), purpose: :checkout) assert_equal data, parse(generate(data, purpose: :checkout), purpose: :checkout)
assert_equal @message, parse(generate(@message, purpose: :checkout), purpose: "checkout") assert_equal data, parse(generate(data, purpose: :checkout), purpose: "checkout")
assert_equal @message, parse(generate(@message, purpose: "checkout"), purpose: :checkout) assert_equal data, parse(generate(data, purpose: "checkout"), purpose: :checkout)
end end
def test_passing_expires_at_sets_expiration_date def test_passing_expires_at_sets_expiration_date
encrypted_message = generate(@message, expires_at: 1.hour.from_now) encrypted_message = generate(data, expires_at: 1.hour.from_now)
travel 59.minutes travel 59.minutes
assert_equal @message, parse(encrypted_message) assert_equal data, parse(encrypted_message)
travel 2.minutes travel 2.minutes
assert_nil parse(encrypted_message) assert_nil parse(encrypted_message)
end end
def test_set_relative_expiration_date_by_passing_expires_in def test_set_relative_expiration_date_by_passing_expires_in
encrypted_message = generate(@message, expires_in: 2.hours) encrypted_message = generate(data, expires_in: 2.hours)
travel 1.hour travel 1.hour
assert_equal @message, parse(encrypted_message) assert_equal data, parse(encrypted_message)
travel 1.hour + 1.second travel 1.hour + 1.second
assert_nil parse(encrypted_message) assert_nil parse(encrypted_message)
...@@ -59,10 +59,10 @@ def test_set_relative_expiration_date_by_passing_expires_in ...@@ -59,10 +59,10 @@ def test_set_relative_expiration_date_by_passing_expires_in
def test_passing_expires_in_less_than_a_second_is_not_expired def test_passing_expires_in_less_than_a_second_is_not_expired
freeze_time do freeze_time do
encrypted_message = generate(@message, expires_in: 1.second) encrypted_message = generate(data, expires_in: 1.second)
travel 0.5.seconds travel 0.5.seconds
assert_equal @message, parse(encrypted_message) assert_equal data, parse(encrypted_message)
travel 1.second travel 1.second
assert_nil parse(encrypted_message) assert_nil parse(encrypted_message)
...@@ -70,19 +70,24 @@ def test_passing_expires_in_less_than_a_second_is_not_expired ...@@ -70,19 +70,24 @@ def test_passing_expires_in_less_than_a_second_is_not_expired
end end
def test_favor_expires_at_over_expires_in def test_favor_expires_at_over_expires_in
payment_related_message = generate(@message, purpose: "payment", expires_at: 2.year.from_now, expires_in: 1.second) payment_related_message = generate(data, purpose: "payment", expires_at: 2.year.from_now, expires_in: 1.second)
travel 1.year travel 1.year
assert_equal @message, parse(payment_related_message, purpose: :payment) assert_equal data, parse(payment_related_message, purpose: :payment)
travel 1.year + 1.day travel 1.year + 1.day
assert_nil parse(payment_related_message, purpose: "payment") assert_nil parse(payment_related_message, purpose: "payment")
end end
def test_skip_expires_at_and_expires_in_to_disable_expiration_check def test_skip_expires_at_and_expires_in_to_disable_expiration_check
payment_related_message = generate(@message, purpose: "payment") payment_related_message = generate(data, purpose: "payment")
travel 100.years travel 100.years
assert_equal @message, parse(payment_related_message, purpose: "payment") assert_equal data, parse(payment_related_message, purpose: "payment")
end end
private
def data
{ "credit_card_no" => "5012-6784-9087-5678", "card_holder" => { "name" => "Donald" } }
end
end end
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册