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
andiv
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.
Comments