mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Allow MessageEncryptor to take advantage of authenticated encryption modes
AEAD modes like `aes-256-gcm` provide both confidentiality and data authenticity, eliminating the need to use MessageVerifier to check if the encrypted data has been tampered with. Signed-off-by: Jeremy Daer <jeremydaer@gmail.com>
This commit is contained in:
parent
d5f57dc227
commit
d4ea18a8cb
3 changed files with 64 additions and 4 deletions
|
@ -1,3 +1,12 @@
|
||||||
|
* Allow MessageEncryptor to take advantage of authenticated encryption modes.
|
||||||
|
|
||||||
|
AEAD modes like `aes-256-gcm` provide both confidentiality and data
|
||||||
|
authenticity, eliminating the need to use MessageVerifier to check if the
|
||||||
|
encrypted data has been tampered with. This speeds up encryption/decryption
|
||||||
|
and results in shorter cipher text.
|
||||||
|
|
||||||
|
*Bart de Water*
|
||||||
|
|
||||||
* Introduce `assert_changes` and `assert_no_changes`.
|
* Introduce `assert_changes` and `assert_no_changes`.
|
||||||
|
|
||||||
`assert_changes` is a more general `assert_difference` that works with any
|
`assert_changes` is a more general `assert_difference` that works with any
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
require 'openssl'
|
require 'openssl'
|
||||||
require 'base64'
|
require 'base64'
|
||||||
require 'active_support/core_ext/array/extract_options'
|
require 'active_support/core_ext/array/extract_options'
|
||||||
|
require 'active_support/message_verifier'
|
||||||
|
|
||||||
module ActiveSupport
|
module ActiveSupport
|
||||||
# MessageEncryptor is a simple way to encrypt values which get stored
|
# MessageEncryptor is a simple way to encrypt values which get stored
|
||||||
|
@ -28,6 +29,16 @@ module ActiveSupport
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
module NullVerifier #:nodoc:
|
||||||
|
def self.verify(value)
|
||||||
|
value
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.generate(value)
|
||||||
|
value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
class InvalidMessage < StandardError; end
|
class InvalidMessage < StandardError; end
|
||||||
OpenSSLCipherError = OpenSSL::Cipher::CipherError
|
OpenSSLCipherError = OpenSSL::Cipher::CipherError
|
||||||
|
|
||||||
|
@ -40,7 +51,8 @@ module ActiveSupport
|
||||||
# Options:
|
# Options:
|
||||||
# * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by
|
# * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by
|
||||||
# <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-cbc'.
|
# <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-cbc'.
|
||||||
# * <tt>:digest</tt> - String of digest to use for signing. Default is +SHA1+.
|
# * <tt>:digest</tt> - String of digest to use for signing. Default is
|
||||||
|
# +SHA1+. Ignored when using an AEAD cipher like 'aes-256-gcm'.
|
||||||
# * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+.
|
# * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+.
|
||||||
def initialize(secret, *signature_key_or_options)
|
def initialize(secret, *signature_key_or_options)
|
||||||
options = signature_key_or_options.extract_options!
|
options = signature_key_or_options.extract_options!
|
||||||
|
@ -48,7 +60,8 @@ module ActiveSupport
|
||||||
@secret = secret
|
@secret = secret
|
||||||
@sign_secret = sign_secret
|
@sign_secret = sign_secret
|
||||||
@cipher = options[:cipher] || 'aes-256-cbc'
|
@cipher = options[:cipher] || 'aes-256-cbc'
|
||||||
@verifier = MessageVerifier.new(@sign_secret || @secret, digest: options[:digest] || 'SHA1', serializer: NullSerializer)
|
@digest = options[:digest] || 'SHA1' unless aead_mode?
|
||||||
|
@verifier = resolve_verifier
|
||||||
@serializer = options[:serializer] || Marshal
|
@serializer = options[:serializer] || Marshal
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -73,20 +86,28 @@ module ActiveSupport
|
||||||
|
|
||||||
# Rely on OpenSSL for the initialization vector
|
# Rely on OpenSSL for the initialization vector
|
||||||
iv = cipher.random_iv
|
iv = cipher.random_iv
|
||||||
|
cipher.auth_data = "" if aead_mode?
|
||||||
|
|
||||||
encrypted_data = cipher.update(@serializer.dump(value))
|
encrypted_data = cipher.update(@serializer.dump(value))
|
||||||
encrypted_data << cipher.final
|
encrypted_data << cipher.final
|
||||||
|
|
||||||
"#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}"
|
blob = "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}"
|
||||||
|
blob << "--#{::Base64.strict_encode64 cipher.auth_tag}" if aead_mode?
|
||||||
|
blob
|
||||||
end
|
end
|
||||||
|
|
||||||
def _decrypt(encrypted_message)
|
def _decrypt(encrypted_message)
|
||||||
cipher = new_cipher
|
cipher = new_cipher
|
||||||
encrypted_data, iv = 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)}
|
||||||
|
raise InvalidMessage if aead_mode? && auth_tag.bytes.length != 16
|
||||||
|
|
||||||
cipher.decrypt
|
cipher.decrypt
|
||||||
cipher.key = @secret
|
cipher.key = @secret
|
||||||
cipher.iv = iv
|
cipher.iv = iv
|
||||||
|
if aead_mode?
|
||||||
|
cipher.auth_tag = auth_tag
|
||||||
|
cipher.auth_data = ""
|
||||||
|
end
|
||||||
|
|
||||||
decrypted_data = cipher.update(encrypted_data)
|
decrypted_data = cipher.update(encrypted_data)
|
||||||
decrypted_data << cipher.final
|
decrypted_data << cipher.final
|
||||||
|
@ -103,5 +124,17 @@ module ActiveSupport
|
||||||
def verifier
|
def verifier
|
||||||
@verifier
|
@verifier
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def aead_mode?
|
||||||
|
@aead_mode ||= new_cipher.authenticated?
|
||||||
|
end
|
||||||
|
|
||||||
|
def resolve_verifier
|
||||||
|
if aead_mode?
|
||||||
|
NullVerifier
|
||||||
|
else
|
||||||
|
MessageVerifier.new(@sign_secret || @secret, digest: @digest, serializer: NullSerializer)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -70,6 +70,24 @@ class MessageEncryptorTest < ActiveSupport::TestCase
|
||||||
assert_not_verified([iv, message] * bad_encoding_characters)
|
assert_not_verified([iv, message] * bad_encoding_characters)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_aead_mode_encryption
|
||||||
|
encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: 'aes-256-gcm')
|
||||||
|
message = encryptor.encrypt_and_sign(@data)
|
||||||
|
assert_equal @data, encryptor.decrypt_and_verify(message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_messing_with_aead_values_causes_failures
|
||||||
|
encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: 'aes-256-gcm')
|
||||||
|
text, iv, auth_tag = encryptor.encrypt_and_sign(@data).split("--")
|
||||||
|
assert_not_decrypted([iv, text, auth_tag] * "--")
|
||||||
|
assert_not_decrypted([munge(text), iv, auth_tag] * "--")
|
||||||
|
assert_not_decrypted([text, munge(iv), auth_tag] * "--")
|
||||||
|
assert_not_decrypted([text, iv, munge(auth_tag)] * "--")
|
||||||
|
assert_not_decrypted([munge(text), munge(iv), munge(auth_tag)] * "--")
|
||||||
|
assert_not_decrypted([text, iv] * "--")
|
||||||
|
assert_not_decrypted([text, iv, auth_tag[0..-2]] * "--")
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def assert_not_decrypted(value)
|
def assert_not_decrypted(value)
|
||||||
|
|
Loading…
Reference in a new issue