I wrote and maintain (though not as attentively as I’d like) a Ruby Gem, Strongbox, which adds Public-key Encryption support to Rails’ ActiveRecord. Simply put, Public-key Encryption is a form of encryption with two password, one to encrypt data and another to decrypt it. This is handy for web applications, any visitor can encrypt data using encryption password (the public key). However, if an attacker gains access to the server and steals the data and the app’s code, they still can’t decrypt the data because they lack the decrypt password (the private key).

The problem is you’re doing it wrong. OK, not all of you. However, I get a fair number of support questions around storing the private key and it’s password on the server. I’ve even see a few tutorials showing how to use Strongbox this way.

If you’re going to do this, don’t use Strongbox. No, I’m not going to get all righteous about how you protect your data, I’m talking about being inefficient.

First, a bit of threat analysis:

  • If you don’t encrypt you data and someone gets you data in any way, then your secrets are exposed.
  • If you encrypt your data and store the decryption password on the server and your data is stolen, say through an SQL injection attack, your secrets are safe. However, if your server is breached, the password can be stolen with your data and thus your secrets.
  • If you encrypt your data using public-key and do not store the unlocked private-key on the server, if your server is breached your secrets are still safe. (Though if the attacker hangs out on your server they could modify your code to capture new secrets.)

If the second option works for you, then you should use Symmetric-key Encryption as it’s much faster and easier than public-key encryption. Symmetric-key Encryption is what people think of when they think of encryption, there’s just one password which both encrypts and decrypts the data.

In Ruby, Symmetric-key encryption is provided by OpenSSL::Cipher (in old Ruby versions it’s OpenSSL::Cipher::Cipher).

First you need to choose an encryption algorithm. You can see the full list with:

ruby -r openssl -e 'puts OpenSSL::Cipher.ciphers'

The simple choice is aes-256-cbc. The Advanced Encryption Standard (AES) is a open encryption standard that is well studied and well understood. It’s what the U.S. Government uses.

That U.S. Government connection makes some people leery of AES. However, it was developed by two Belgian cryptographers, the winner of a very public challenge, and the professionals believe it a good choice. However, this is security, don’t take my word for it. Do the research and especially look at discussions around AES vs [Blowfish](http://en.wikipedia.org/wiki/Blowfish_(cipher) and Twofish

In the string aes-256-cbc the 256 is the key (password) size in bits. AES supports 128, 192 and 256 bit keys. Unless you are running on a device without much CPU, there’s no reason to not use 256 bits (32 bytes).

cbc stands for Cipher-block chaining. Ciphers can only encrypt data in small chunks, called blocks, which are glued together to form the whole of the cipher text. CBC is the glue. To ensure that blocks containing the same data are encrypted differently, some randomness is need. For block cipher this randomness is the Initialization vector (IV). A good, detail explanation of block ciphers and the IV can be found here.

Back to the code. To use OpenSSL::Cipher, we need to instantiate a cipher and provide it with a key and an IV.

require 'openssl'
cipher = OpenSSL::Cipher.new('aes-256-cbc')
cipher.encrypt # We are encrypting
key = cipher.random_key
iv = cipher.random_iv

#encrypt (and #decrypt) sets the mode we are working in. You must call it before calling key=, iv=, #random_key, or #random_iv. The cipher instance will not return the key or the IV once they are set, which it why we’re saving them in the key and iv variables.

If you create your own key, make sure it’s 256 bits long, short keys raise OpenSSL::Cipher::CipherError: key length too short. Keys longer than 256 bits are truncated and work, but are likely bite you sometime.

Once the cipher is configured we can encrypt:

encrypted_string = cipher.update 'This is a secret'
encrypted_string << cipher.final

#update encrypts the text passed to it. You can call it more that once if you want to encrypt text in chunks to avoid file slurping:

encrypted_string = ''
File.foreach('plaintext') do |line|
  encrypted_string << cipher.update line
end
encrypted_string << cipher.final

#final flushes the cipher object. The data is encrypted in fixed size blocks. If the data passed to #update is not exactly divisible by the block size, some will be left in the buffer. Calling #final pads out the remaining data to the block size, encrypts, and returns it. Calling #final a second time, or calling #update after calling #final will return garbage, so don’t.

To decrypt:

cipher = OpenSSL::Cipher.new('aes-256-cbc')
cipher.decrypt
cipher.key = key
cipher.iv = iv
decrypted_string = cipher.update(encrypted_string)
decrypted_string << cipher.final

If something goes wrong, you’ll get an unhelpful (but secure) OpenSSL::Cipher::CipherError when calling #final. It’s going to be one two things: you have the wrong key or you forgot to call #final when encrypting. If you instead get random garbage, then you have the wrong IV.

How you use this in a Rails app?

class Secret < ActiveRecord::Base
  def secret_data
    return '' unless  self.encrypted_data
    cipher = OpenSSL::Cipher.new('aes-256-cbc')
    cipher.decrypt
    cipher.key = ENV['SECRET']
    cipher.iv = self.iv

    decrypted_data = cipher.update(read_attribute(:secret_data))
    decrypted_data << cipher.final
  end

  def secret_data=(data)
    cipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
    cipher.encrypt
    cipher.key = ENV['SECRET']
    self.iv = cipher.random_iv

    encrypted_data = cipher.update(data)
    encrypted_data << cipher.final
	write_attribute(:secret_data, encrypted_data)
    data
  end

  def clear_secret!
	write_attribute(:secret_data, nil)
    self.iv = nil
  end
end

Notes:

  • secret_data and iv need to be a binary columns or your data will be lost. Alternatively, Base64 encode then first.

  • This assumes you set the key in the environment, which may not be the best approach.

  • #clear_secret! is a convenience method to bypass the encryption in the setter remove the encrypted data.

So, if you’re comfortable with storing your encryption key on your server, save the public-key overhead and skip right to symmetric-key encryption. Leave a comment if you’d like to see this turned into a gem.

Safe image some rights reserved by Jim Sage.

Comments