Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Custom serializers and deserializers in MessageVerifier and MessageEncryptor. #3031

Merged
merged 2 commits into from

3 participants

Willem van Bergen Jon Leighton José Valim
Willem van Bergen

By default, these ActiveSupport classes use Marshal for serializing and deserializing messages. Unfortunately, the Marshal format is closely associated with Ruby internals. This makes the resulting message very hard to unserialize messages generated by these classes in other environments like node.js.

This patch solves this by allowing you to set your own custom serializer and deserializer lambda functions. By default, it still uses Marshal to be backwards compatible. Simple example:

verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!")
verifier.serializer = lambda { |value| ActiveSupport::JSON.encode(value) }
verifier.deserializer = lambda { |value| ActiveSupport::JSON.decode(value) }

message = verifier.generate({ :foo => 123, 'bar' => Time.local(2010) })
p verifier.verify(message) # => { "foo" => 123, "bar" => "2010-01-01T00:00:00-05:00" }

Note that based on the serializer and deserializer, it may not be possible to do a lossless roundtrip as seen in the example. That might not be a problem depending on your needs, but using Marshal as the default makes sense so users don't have to worry about that.

The way I implemented it may not be optimal, so I welcome any feedback!

wvanbergen added some commits
Willem van Bergen wvanbergen Custom serializers and deserializers in MessageVerifier and MessageEn…
…cryptor.

By default, these classes use Marshal for serializing and deserializing messages. Unfortunately, the Marshal format is closely associated with Ruby internals and even changes between different interpreters. This makes the resulting message very hard to impossible to unserialize messages generated by these classes in other environments like node.js.

This patch solves this by allowing you to set your own custom serializer and deserializer lambda functions. By default, it still uses Marshal to be backwards compatible.
bffaa88
Willem van Bergen wvanbergen Fixed tests so that they will also run properly in other timezones. a8aaef6
José Valim josevalim merged commit 06c3ca8 into from
Jon Leighton
Collaborator

Hey, I know this has already been merged now, but I think a better API would be:

ActiveSupport::MessageVerifier.new("Hey, I'm a secret!", serializer)

Where serializer is an object that responds to load and dump. So the default would be:

ActiveSupport::MessageVerifier.new("Hey, I'm a secret!", Marshal)

A JSON serializer would be:

class JSONSerializer
  def load(value)
    ActiveSupport::JSON.decode(value)
  end

  def dump(value)
    ActiveSupport::JSON.encode(value)
  end
end

This is better because:

  • It relies on only one parameter, rather than two
  • The parameter is set in the initializer. I don't think the serialization strategy should change once the object has been created.
  • The load/dump requirement is the same as the new serialize API in Active Record, so the same serialization objects could be used.
José Valim
Owner

I agree. @wvanbergen could you send another pull request? pretty please with sugar on top? :D

Willem van Bergen

The problem with this is that the second parameter is already in use for the hash/encryption algorithm to use. I don't expect many people to ever use this parameter because the default value is usually what you want. It would break backwards compatibility to change this though.

So I can add it as a third parameter which requires people to explicitly provide any time they want to change the serializer, or do some magic parameter type detection to make this work. I like neither of these solutions, but I will implement whatever you think is the best of those two evils :).

José Valim
Owner

It is fine to be a third argument but for convenience we should also have attr_accessor as well. So you could do:

message_verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!")
message_verifier.serializer = JSONSerializer

I guess this is the best of both worlds, no?

Willem van Bergen

Works for me. Give me one sec.

Willem van Bergen

Voilà. See #3035.

Jon Leighton
Collaborator

I am still not a massive fan of specifying it via attr_accessor. I think in an ideal world we'd use an options hash, e.g.

ActiveSupport::MessageVerifier.new(
  "Hey, I'm a secret!",
  :serializer => JSONSerializer,
  :cipher     => 'bcrypt'
)

It would be possible to implement this with a deprecation for if the second argument is not a hash. What do you guys think?

Jon Leighton
Collaborator

Also, can the docs/changelog be updated please? (Once we've decided out what to do.)

Willem van Bergen

I like that too, but it still breaks backwards compatibility, unless we do some magic type checking on the second parameter.

Jon Leighton
Collaborator

Yeah, that's what I am suggesting:

def initialize(secret, options = {})
  unless options.respond_to?(:keys)
    ActiveSupport::Deprecation.warn "bla bla"
    options = { :cipher => options }
  end

  options = { cipher => '...', :serializer => Marshal) }.merge(options)
end
Willem van Bergen

I have to code written for this patch. @josevalim: do you want me to create another pull request with these changes or do you want to keep it like it is right now?

José Valim
Owner

Using a hash on initialize is better. Pull request pls!

Willem van Bergen

All merged.

For those interested, a compatible MessageVerifier implementation for node.js: https://gist.github.com/1220401

Steve Agalloco stve referenced this pull request from a commit
Josh Kalderimis joshk Updated the json date regex to recognize xmlschema formatted date tim…
…es during json decoding. [#3031 state:resolved]

Signed-off-by: Santiago Pastorino and Emilio Tagua <santiago+emilioe@wyeworks.com>
e8c8707
Pascal Friederich paukul referenced this pull request from a commit in paukul/rails
Josh Kalderimis joshk Updated the json date regex to recognize xmlschema formatted date tim…
…es during json decoding. [#3031 state:resolved]

Signed-off-by: Santiago Pastorino and Emilio Tagua <santiago+emilioe@wyeworks.com>
73b9e43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Sep 15, 2011
  1. Willem van Bergen

    Custom serializers and deserializers in MessageVerifier and MessageEn…

    wvanbergen authored
    …cryptor.
    
    By default, these classes use Marshal for serializing and deserializing messages. Unfortunately, the Marshal format is closely associated with Ruby internals and even changes between different interpreters. This makes the resulting message very hard to impossible to unserialize messages generated by these classes in other environments like node.js.
    
    This patch solves this by allowing you to set your own custom serializer and deserializer lambda functions. By default, it still uses Marshal to be backwards compatible.
  2. Willem van Bergen
This page is out of date. Refresh to see the latest.
8 activesupport/lib/active_support/message_encryptor.rb
View
@@ -13,9 +13,13 @@ class MessageEncryptor
class InvalidMessage < StandardError; end
OpenSSLCipherError = OpenSSL::Cipher.const_defined?(:CipherError) ? OpenSSL::Cipher::CipherError : OpenSSL::CipherError
+ attr_accessor :serializer, :deserializer
+
def initialize(secret, cipher = 'aes-256-cbc')
@secret = secret
@cipher = cipher
+ @serializer = lambda { |value| Marshal.dump(value) }
+ @deserializer = lambda { |value| Marshal.load(value) }
end
def encrypt(value)
@@ -27,7 +31,7 @@ def encrypt(value)
cipher.key = @secret
cipher.iv = iv
- encrypted_data = cipher.update(Marshal.dump(value))
+ encrypted_data = cipher.update(serializer.call(value))
encrypted_data << cipher.final
[encrypted_data, iv].map {|v| ActiveSupport::Base64.encode64s(v)}.join("--")
@@ -44,7 +48,7 @@ def decrypt(encrypted_message)
decrypted_data = cipher.update(encrypted_data)
decrypted_data << cipher.final
- Marshal.load(decrypted_data)
+ deserializer.call(decrypted_data)
rescue OpenSSLCipherError, TypeError
raise InvalidMessage
end
8 activesupport/lib/active_support/message_verifier.rb
View
@@ -21,9 +21,13 @@ module ActiveSupport
class MessageVerifier
class InvalidSignature < StandardError; end
+ attr_accessor :serializer, :deserializer
+
def initialize(secret, digest = 'SHA1')
@secret = secret
@digest = digest
+ @serializer = lambda { |value| Marshal.dump(value) }
+ @deserializer = lambda { |value| Marshal.load(value) }
end
def verify(signed_message)
@@ -31,14 +35,14 @@ def verify(signed_message)
data, digest = signed_message.split("--")
if data.present? && digest.present? && secure_compare(digest, generate_digest(data))
- Marshal.load(ActiveSupport::Base64.decode64(data))
+ deserializer.call(ActiveSupport::Base64.decode64(data))
else
raise InvalidSignature
end
end
def generate(value)
- data = ActiveSupport::Base64.encode64s(Marshal.dump(value))
+ data = ActiveSupport::Base64.encode64s(serializer.call(value))
"#{data}--#{generate_digest(data)}"
end
10 activesupport/test/message_encryptor_test.rb
View
@@ -8,6 +8,7 @@
else
require 'active_support/time'
+require 'active_support/json'
class MessageEncryptorTest < Test::Unit::TestCase
def setup
@@ -38,7 +39,14 @@ def test_signed_round_tripping
message = @encryptor.encrypt_and_sign(@data)
assert_equal @data, @encryptor.decrypt_and_verify(message)
end
-
+
+ def test_alternative_serialization_method
+ @encryptor.serializer = lambda { |value| ActiveSupport::JSON.encode(value) }
+ @encryptor.deserializer = lambda { |value| ActiveSupport::JSON.decode(value) }
+
+ message = @encryptor.encrypt_and_sign({ :foo => 123, 'bar' => Time.utc(2010) })
+ assert_equal @encryptor.decrypt_and_verify(message), { "foo" => 123, "bar" => "2010-01-01T00:00:00Z" }
+ end
private
def assert_not_decrypted(value)
9 activesupport/test/message_verifier_test.rb
View
@@ -8,6 +8,7 @@
else
require 'active_support/time'
+require 'active_support/json'
class MessageVerifierTest < Test::Unit::TestCase
def setup
@@ -31,6 +32,14 @@ def test_tampered_data_raises
assert_not_verified("#{data}--#{hash.reverse}")
assert_not_verified("purejunk")
end
+
+ def test_alternative_serialization_method
+ @verifier.serializer = lambda { |value| ActiveSupport::JSON.encode(value) }
+ @verifier.deserializer = lambda { |value| ActiveSupport::JSON.decode(value) }
+
+ message = @verifier.generate({ :foo => 123, 'bar' => Time.utc(2010) })
+ assert_equal @verifier.verify(message), { "foo" => 123, "bar" => "2010-01-01T00:00:00Z" }
+ end
def assert_not_verified(message)
assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do
Something went wrong with that request. Please try again.