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!)
# 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

Done! You can use EncryptedCoder in any model.

A quick demo:

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"

About the author

Kir Shatrov Kir Shatrov helps businesses to grow by scaling the infrastructure. He likes to write about software, scalability and interesting stories that he runs into at work. Follow him on Twitter to get the latest updates: @kirshatrov.

Comments

comments powered by Disqus