HELLO, I’M SERHAT AND THIS IS MY FANCY TITLE.

Encrypting Sensitive Data with Ruby on Rails

The most recent versions (5.1 and 5.2) of Ruby on Rails shipped with a feature named as encrypted credentials which replaces the secrets.yml feature and enables us to keep sensitive data in an encrypted file named as config/credentials.yml.enc. With this new feature, pushing the encrypted file to the repository wasn't an issue anymore - and in fact Rails encouraged users to push credentials.yml.enc.

However, this feature only works with a single file which is config/credentials.yml.enc. Recently we needed to add some seed-like data, which doesn't fit to credentials.yml.enc to our web application.

  • The seed data included some user information such as date of birth information, last names etc. These are not top-secret information, and they don't include critical data such as passwords etc, but it's better to keep them away from curious eyes.
  • We couldn't simply use dummy ID numbers because the application was verifying ID numbers over the government APIs, and the app can't work with fake ID numbers in general.
  • On the other hand, since we are making autonomous deployments with CircleCI and Dokku, transfering the seed file to production server after each deploy wasn't an option.
  • Of course, there are lots of professional solutions to encrypt a file and decrypt it when needed, but we didn't want to implement another complexity level or another dependency (such as GEM) to the app. Therefore we aimed to solve this problem with core capabilities of Ruby and Rails.

After taking a look to Rails source code, roktas and me decided to imitate the behaviour of encrypted credentials, and we wrote a simple wrapper calling core ActiveSupport methods. Here is our very simple encryptor and decryptor code, located under lib/support/file_encryptor.rb:

# frozen_string_literal: true

module FileEncryptor
  DEFAULT_PARAMS = {
    env_key: 'RAILS_MASTER_KEY',
    key_path: Rails.root.join('config', 'master.key'),
    raise_if_missing_key: true
  }.freeze

  def self.encrypt(path)
    encryptor = ActiveSupport::EncryptedFile.new(
      merge_with_content_path(Rails.root.join('db', 'encrypted_data', path.split('/').last + '.enc'))
    )

    encryptor.write(File.read(Rails.root.join(path)))
  end

  def self.decrypt(path)
    encryptor = ActiveSupport::EncryptedFile.new(
      merge_with_content_path(Rails.root.join(path))
    )

    encryptor.read
  end

  def self.decrypt_lines(path)
    decrypt(path).split("\n")
  end

  def self.merge_with_content_path(value)
    DEFAULT_PARAMS.merge(
      content_path: value
    )
  end
end

As you might see in the first look, it wraps the ActiveSupport::EncryptedFile and behaves the same. You can use either an environment variable or the master.key for decrypting encrypted files. It works with absolute and relative paths, and so on. Here are some examples:

  • Encrypt a file by passing a relative path as an argument:
FileEncryptor.encrypt('db/static_data/users.csv')
  • Encrypt a file by passing an absolute path as an argument:
FileEncryptor.encrypt('/lib/important_files/foobar.csv')')
  • Decrypt an encrypted file as a whole by passing a relative path as an argument:
FileEncryptor.decrypt('db/encrypted_data/users.csv.enc')
  • Decrypt an encrypted file as array by passing an absolute path as an argument:
FileEncryptor.decrypt_lines('/lib/top_secret_files/users.csv.enc')

With decrypt_linesmethod we became able to iterate on users in seeds.rb:

users = FileEncryptor.decrypt_lines('/lib/top_secret_files/users.csv.enc')
users.each do |user|
  User.create(...)
end

No magic, no dependencies, no GEMs, no packages. Just native capabilities of Ruby on Rails.

Cheers.

Date:
Categories: tech, ruby on rails

Share this post!


Blog Comments powered by Disqus.