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

Encrypting Sensitive Data with Ruby on Rails

First of all:

When possible, avoid putting sensitive data to your Github repository, even if it's encrypted and even if your repository is private. This post doesn't explain a 'best practice' or something like that, it just includes a 'rare case' that we come across and our solution to it. I just published this to get ideas and alternatives from the community about our solution to a very specific problem. At the end of the day I would be happy to hear a better alternative and update this post.

In general, keeping sensitive data in Github is considered to be a bad practice, even if you keep the repository private. However, in rare cases, you might need to keep some credentials in the repository with various reasons, such as making the app quickly deployable over a PaaS service, making the setup process easier for potential contributors, seeding the application with some initial data and so on.

Fortunately, the most recent versions (version 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.

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 identity numbers, 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 write 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.