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"

Comments

comments powered by Disqus