Simple encryption of ActiveRecord fields

For past week, I have been working on encryption solution for a Rails app. The requirement was to encrypt chosen fields like ssn of an ActiveRecord model.

I've research a variety of solutions, including attr_encrypted and encryptor gems.

I want to show a simple way of encryption that combines ActiveRecord::Base.serialize and OpenSSL::Cipher, which comes with the Ruby stdlib.

Few things to bear in mind:

  • this kind of encryption helps only in case when your database is stolen
  • if hacker gets access to Rails console or ENV['ENCRYPTION_KEY'], you're hacked
  • you may want to use IV and salt for sensitive data
  • by using Marshal, our encrypted field can store instance of any class (Date, Time, whatever!)

{% highlight ruby %}

lib/crypt.rb

module Crypt class << self def encrypt(value) crypt(:encrypt, value) end

def decrypt(value)
  crypt(:decrypt, value)
end

def encryption_key
  ENV.fetch('ENCRYPTION_KEY')
end

ALGO = 'aes-256-cbc'.freeze
def crypt(cipher_method, value)
  cipher = OpenSSL::Cipher::Cipher.new(ALGO)
  cipher.send(cipher_method)
  cipher.pkcs5_keyivgen(encryption_key)
  result = cipher.update(value)
  result << cipher.final
end

end end

lib/encrypted_coder.rb

custom coder for Rails serialized attribute

more examples: https://github.com/rails/rails/tree/4-2-stable/activerecord/lib/active_record/coders

encrypted value has to be stored as base64 because it's not UTF-safe

class EncryptedCoder def load(value) return if value.nil?

Marshal.load(
  Crypt.decrypt(
    Base64.decode64(value)))

end

def dump(value) Base64.encode64( Crypt.encrypt( Marshal.dump(value))) end end

app/models/wow_such_secure_model.rb

class WowSuchSecureModel < ActiveRecord::Base serialize :ssn, EncryptedCoder.new end {% endhighlight %}

Done! You can use EncryptedCoder in any model.

A quick demo:

{% highlight ruby %} pry(main)> model = WowSuchSecureModel.create(ssn: "11-22-333") (0.2ms) BEGIN SQL (13.2ms) INSERT INTO "table" ("ssn", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["ssn", "S9CTpTxsuG1mFExrFzyy1XD1qtxpiTKGOiopvFhuuwY=\n"], ["created_at", "2015-12-18 21:52:24.425346"], ["updated_at", "2015-12-18 21:52:24.425346"]] (7.5ms) COMMIT => #<WowSuchSecureModel:0x007f803a19f4f8 id: 4, ssn: "11-22-333", created_at: Fri, 18 Dec 2015 21:52:24 UTC +00:00, updated_at: Fri, 18 Dec 2015 21:52:24 UTC +00:00> pry(main)> model.ssn => "11-22-333" pry(main)> WowSuchSecureModel.last.ssn => "11-22-333" pry(main)> WowSuchSecureModel.last.ssn_before_type_cast => "S9CTpTxsuG1mFExrFzyy1XD1qtxpiTKGOiopvFhuuwY=\n" {% endhighlight %}

Written in December 2015.
Kir Shatrov

Kir Shatrov helps businesses to grow by scaling the infrastructure. He writes about software, scalability and the ecosystem. Follow him on Twitter to get the latest updates.