Skip to content

Commit

Permalink
Allow MessageEncryptor to take advantage of authenticated encryption …
Browse files Browse the repository at this point in the history
…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>
  • Loading branch information
bdewater authored and jeremy committed Jul 21, 2016
1 parent d5f57dc commit d4ea18a
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 4 deletions.
9 changes: 9 additions & 0 deletions activesupport/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`.

`assert_changes` is a more general `assert_difference` that works with any
Expand Down
41 changes: 37 additions & 4 deletions activesupport/lib/active_support/message_encryptor.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'openssl'
require 'base64'
require 'active_support/core_ext/array/extract_options'
require 'active_support/message_verifier'

module ActiveSupport
# MessageEncryptor is a simple way to encrypt values which get stored
Expand Down Expand Up @@ -28,6 +29,16 @@ def self.dump(value)
end
end

module NullVerifier #:nodoc:
def self.verify(value)
value
end

def self.generate(value)
value
end
end

class InvalidMessage < StandardError; end
OpenSSLCipherError = OpenSSL::Cipher::CipherError

Expand All @@ -40,15 +51,17 @@ class InvalidMessage < StandardError; end
# Options:
# * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by
# <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+.
def initialize(secret, *signature_key_or_options)
options = signature_key_or_options.extract_options!
sign_secret = signature_key_or_options.first
@secret = secret
@sign_secret = sign_secret
@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
end

Expand All @@ -73,20 +86,28 @@ def _encrypt(value)

# Rely on OpenSSL for the initialization vector
iv = cipher.random_iv
cipher.auth_data = "" if aead_mode?

encrypted_data = cipher.update(@serializer.dump(value))
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

def _decrypt(encrypted_message)
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.key = @secret
cipher.iv = iv
if aead_mode?
cipher.auth_tag = auth_tag
cipher.auth_data = ""
end

decrypted_data = cipher.update(encrypted_data)
decrypted_data << cipher.final
Expand All @@ -103,5 +124,17 @@ def new_cipher
def verifier
@verifier
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
18 changes: 18 additions & 0 deletions activesupport/test/message_encryptor_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,24 @@ def test_message_obeys_strict_encoding
assert_not_verified([iv, message] * bad_encoding_characters)
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

def assert_not_decrypted(value)
Expand Down

0 comments on commit d4ea18a

Please sign in to comment.