The two methods we will be examining in detail here will be encrypt_and_sign and its counterpart, decrypt_and_verify.

How It Works

Follow along: qw activesupport

Open up lib/activesupport/message_encryptor.rb, and let's start reading. Hop on down to where ActiveSupport defines its exceptions. In past articles, we've seen how easy it is to create custom exception classes, but there's another interesting trick here:

OpenSSLCipherError = OpenSSL::Cipher::CipherError

OpenSSL::Cipher::CipherError is being assigned to OpenSSLCipherError. This provides a shortcut for referencing OpenSSL's exception later. It's a negligible savings, but it demonstrates how malleable Ruby is.

MessageEncryptor's initializer is a bit more complicated that you might expect from the documentation. The only documented options are secret, :cipher, and :serializer, but you can also pass in a custom sign_secret that is used for the MessageVerifier. Since it isn't documented, you probably don't want to rely on this, but as an exercise lets see how it gets set.

signature_key_or_options will be an array with zero or more values. As we've seen previouslyextract_options! will pop a hash of options off the end of an array if present. Next sign_secret gets the first value from signature_key_or_options. If the the array is empty, sign_secret will be nil. Let's see some examples:

We saw that MessageVerifier could take anything that responded to load and dump and use it as a serializer. Although this conforms to the interface MessageVerifier expects, it seems quite strange. To understand the point of this, take a look at encrypt_and_sign:

Reading this inside out, we first encrypt the value, and then verify it. In order to encrypt it, MessageEncryptor will serialize the data so there's no point in MessageVerifier serializing it again. Although a bit confusing, this demonstrates the power of both duck typing, and the Strategy Pattern.

We've already seen MessageVerifier#generate, let's see how _encrypt is implemented:

Ruby's OpenSSL library is a bit awkward to work with. After creating a Cipher, you need to configure it. For instance you need to either call encrypt or decrypt to set the Cipher's mode. This API closely mirrors the C implementation, but what may be idiomatic in one language does not always translate well to another. Sometimes when reading code, it's worth imagining how you might have written something:

Notice that it doesn't always return anything. This is because the encryption algorithm needs to buffer up data before it can encrypt it. Calling final forces it to emit whatever is left. Beware, OpenSSL does not check to make sure that you call final, and it also does not prevent you from calling update after final. In both these cases, it will simply return garbage.

At the very end of _encrypt the encrypted data is Base64 encoded along with the initialization vector using the same scheme we saw in MessageVerifier.

decrypt_and_verify undoes the effects of encrypt_and_sign as you might expect. First it verifies the data hasn't been tampered with using MessageVerifier, and then it calls _decrypt. The _decrypt method follows the same pattern as _encrypt, but more or less in reverse.

There you have it, symmetric encryption in Rails.

Recap

We have seen how Rails can sign and encrypt our data using MessageEncryptor. We also saw an example of how Rails uses the Strategy Pattern when serializing data.

You may never use OpenSSL directly, but if you do, you can learn from MessageEncryptor:

OpenSSL::Cipher needs to have its mode set

You need to buffer the output of OpenSSL::Cipher, and call final

OpenSSL returns garbage if you call update after final

Luckily for us, MessageEncryptor takes care of these details.

Follow along: qw actionpack and qw activesupport

If you want to dig deeper, look at how ActionDispatch::EncryptedCookieJar uses MessageEncryptor, or read up on how ActiveSupport::KeyGenerator allows you to use the same secret key in different contexts.

As always let me know if I missed anything, or if you have any questions.