From 07abc5efe1bc71902b0c517ef97dcb36564f2336 Mon Sep 17 00:00:00 2001 From: Michael Koziarski Date: Tue, 25 Nov 2008 20:27:54 +0100 Subject: Add a MessageEncryptor, just like MessageVerifier but using symmetric key encryption. The use of encryption prevents people from seeing any potentially secret values you've used. It also supports and encrypt_and_sign model to prevent people from tampering with the bits and creating random junk that gets fed to A motivated coder could use this to add an :encrypt=>true option to the cookie store. --- activesupport/CHANGELOG | 2 +- activesupport/lib/active_support.rb | 1 + .../lib/active_support/message_encryptor.rb | 69 ++++++++++++++++++++++ activesupport/test/message_encryptor_test.rb | 46 +++++++++++++++ 4 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 activesupport/lib/active_support/message_encryptor.rb create mode 100644 activesupport/test/message_encryptor_test.rb (limited to 'activesupport') diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG index 755ec246ab..9e7dc458b4 100644 --- a/activesupport/CHANGELOG +++ b/activesupport/CHANGELOG @@ -1,6 +1,6 @@ *2.3.0 [Edge]* -* Added ActiveSupport::MessageVerifier to aid users who need to store signed messages. [Koz] +* Added ActiveSupport::MessageVerifier and MessageEncryptor to aid users who need to store signed and/or encrypted messages. [Koz] * Added ActiveSupport::BacktraceCleaner to cut down on backtrace noise according to filters and silencers [DHH] diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 3758f63eb0..8729d6de59 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -38,6 +38,7 @@ module ActiveSupport autoload :Gzip, 'active_support/gzip' autoload :Inflector, 'active_support/inflector' autoload :Memoizable, 'active_support/memoizable' + autoload :MessageEncryptor, 'active_support/message_encryptor' autoload :MessageVerifier, 'active_support/message_verifier' autoload :Multibyte, 'active_support/multibyte' autoload :OptionMerger, 'active_support/option_merger' diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb new file mode 100644 index 0000000000..de2b4bee76 --- /dev/null +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -0,0 +1,69 @@ +require 'openssl' + +module ActiveSupport + # MessageEncryptor is a simple way to encrypt values which get stored somewhere + # you don't trust. + # + # The cipher text and initialization vector are base64 encoded and returned to you. + # + # This can be used in situations similar to the MessageVerifier, but where you don't + # want users to be able to determine the value of the payload. + class MessageEncryptor + class InvalidMessage < StandardError; end + + def initialize(secret, cipher = 'aes-256-cbc') + @secret = secret + @cipher = cipher + end + + def encrypt(value) + cipher = new_cipher + # Rely on OpenSSL for the initialization vector + iv = cipher.random_iv + + cipher.encrypt + cipher.key = @secret + cipher.iv = iv + + encrypted_data = cipher.update(Marshal.dump(value)) + encrypted_data << cipher.final + + [encrypted_data, iv].map {|v| ActiveSupport::Base64.encode64s(v)}.join("--") + end + + def decrypt(encrypted_message) + cipher = new_cipher + encrypted_data, iv = encrypted_message.split("--").map {|v| ActiveSupport::Base64.decode64(v)} + + cipher.decrypt + cipher.key = @secret + cipher.iv = iv + + decrypted_data = cipher.update(encrypted_data) + decrypted_data << cipher.final + + Marshal.load(decrypted_data) + rescue OpenSSL::CipherError, TypeError + raise InvalidMessage + end + + def encrypt_and_sign(value) + verifier.generate(encrypt(value)) + end + + def decrypt_and_verify(value) + decrypt(verifier.verify(value)) + end + + + + private + def new_cipher + OpenSSL::Cipher::Cipher.new(@cipher) + end + + def verifier + MessageVerifier.new(@secret) + end + end +end \ No newline at end of file diff --git a/activesupport/test/message_encryptor_test.rb b/activesupport/test/message_encryptor_test.rb new file mode 100644 index 0000000000..c0b4a4658c --- /dev/null +++ b/activesupport/test/message_encryptor_test.rb @@ -0,0 +1,46 @@ +require 'abstract_unit' + +class MessageEncryptorTest < Test::Unit::TestCase + def setup + @encryptor = ActiveSupport::MessageEncryptor.new(ActiveSupport::SecureRandom.hex(64)) + @data = {:some=>"data", :now=>Time.now} + end + + def test_simple_round_tripping + message = @encryptor.encrypt(@data) + assert_equal @data, @encryptor.decrypt(message) + end + + def test_encrypting_twice_yields_differing_cipher_text + first_messqage = @encryptor.encrypt(@data) + second_message = @encryptor.encrypt(@data) + assert_not_equal first_messqage, second_message + end + + def test_messing_with_either_value_causes_failure + text, iv = @encryptor.encrypt(@data).split("--") + assert_not_decrypted([iv, text] * "--") + assert_not_decrypted([text, munge(iv)] * "--") + assert_not_decrypted([munge(text), iv] * "--") + assert_not_decrypted([munge(text), munge(iv)] * "--") + end + + def test_signed_round_tripping + message = @encryptor.encrypt_and_sign(@data) + assert_equal @data, @encryptor.decrypt_and_verify(message) + end + + + private + def assert_not_decrypted(value) + assert_raises(ActiveSupport::MessageEncryptor::InvalidMessage) do + @encryptor.decrypt(value) + end + end + + def munge(base64_string) + bits = ActiveSupport::Base64.decode64(base64_string) + bits.reverse! + ActiveSupport::Base64.encode64s(bits) + end +end -- cgit v1.2.3