A simple way to encrypt and decrypt in Rails 5 - Revisited

Two years ago today, I came up with a "not so great" idea, ever. Two years ago I saw myself as a young upcoming developer that turned a blind eye on security. I had no real world hands on experience. I only had the experience gained from my own findings while creating side projects.

The "not so great" idea

I recently heard about Shopify apps. I was excited to, at least, get an app in their app store. Since I liked numbers and accounting, I thought I'd do something with Xero. I wanted to use Xero's Partner Application instead of the Public Application. Going the Partner route seemed too long; I needed to have a landing page setup with reviews and a bunch of other things in place before I was eligible. By having a partner application, I could extend the logged in session between my Shopify app and Xero. The app I created essentially generates an invoice for in Xero. This means this was done automatically behind the scenes. The "not so great" idea came into play where a user would upload a privatekey.pem  file and also their Consumer Key and Consumer Secret 🤦🏽‍♂️ Not that great, right? The idea was squashed but I'll still demonstrate how it's done 😅

Let's dive in!‌

# We're creating a simple rails application in our chosen directory
# Ensure that we have a database called encrypt_decrypt_form_development
rails new encrypt_decrypt_form --database="postgresql"

# Generate a controller
rails g controller users

‌With that in place, we have a basic rails application.


We'll need some basic authentication in place for our users, right? Not needed but we'll implement one. For this, we'll use a popular gem called Devise.  Add to your Gemfile then bundle install:After, we should see something like below in our terminal. We'll need to implement this later. You can implement the flash notice on your own 😉‌

Some setup you must do manually if you haven't yet:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

     In production, :host should be set to the actual host of your application.

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

  4. You can copy Devise views (for customization) to your app by running:

       rails g devise:views

‌In this application, we want a user model. A simple model that holds an email address, consumer_key and consumer_secret key (encrypted).‌

rails generate devise user

‌Devise generates some default table columns. You can inspect and uncomment the columns as you like. For now, we'll keep things simple as is. Next, run rails db:migrate to create the table. Restart your rails server and ensure your application still works. If not, check your terminal for any errors. At this point, there shouldn't be any errors.Previously we generated the users controller users_controller.rb. Rails also created an users folder in our views folder. Inside that users folder, create an index.html.erb file and then in our route.rb file we'll use that file as our default homepage:‌

Rails.application.routes.draw do
  devise_for :users
  # Homepage route: localhost:3000
  # Controller: users
  # Method: index
  root to: 'users#index'

  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html

‌Refresh your browser and you'll see a blank white page. Perfect!‌

class UsersController < ApplicationController
  before_action :authenticate_user!
  include CryptConcern

  # GET /
  def index
    @user = current_user
  # PATCH /save
  def save_keys
    user = current_user

    # To see the decrypted value:
    # puts crypt.decrypt_and_verify(user.consumer_key)
    # will print the value in your terminal.

    # If all is saved, return to homepage
    user.update_attributes(user_params) and return redirect_to root_path

    # Normally this action would be an :edit
    # Ensure you have an edit method before changing.
    render action: :index

    def user_params
      params.require(:user).permit(:consumer_key, :consumer_secret)

‌Our homepage lives at the method index where we return the currently logged in user. We use Devise's before_action :authenticate_user! to force a user to log in before accessing the homepage else they are required to log in or register. We included a shared method inside our users controller from a module called CryptConcern, we'll dive into that bit later, which shares some logic between our user.rb model and users_controller.rb controller. When we update a user, both input fields are encrypted before save. When saved, we redirect the user back to the homepage. I'm sure you'd change this logic to suit your needs. Lastly, we need to update our routes.rb with our patch route:

Rails.application.routes.draw do
  devise_for :users
  root to: 'users#index'

  patch '/save', to: 'users#save_keys', as: 'save_keys'
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html


# This module sole purpose is to encrypt/decrypt sensitive data which stores
# in our database.
# Located in /models/concerns/crypt_concern.rb

module CryptConcern
  extend ActiveSupport::Concern

  def crypt
    len = ActiveSupport::MessageEncryptor.key_len
    # Ensute to have a :salt key in your credentials
    # with: EDITOR='vi' bundle exec rails credentials:edit
    salt = Rails.application.credentials[:salt]
    key = ActiveSupport::KeyGenerator.new(salt).generate_key(salt, len)

    # Value of last executed line is returned

‌If we look back at ActiveSupport::MessageEncryptor, you'll noticed I changed up a line, salt. I needed to persist SecureRandom.random_bytes(32) but having an issue when copied/pasted: characters were changing. Therefore, I went with SecureRandom.base64(32):Let's generate a salt string in our terminal:‌

# Copy the result. Yours will be different:
#=> McQIX4LnIRhZ4BUHB9lRxnu++TnPMv2XLjJIxvzSJg8=

# Then
EDITOR='vi' bundle exec rails credentials:edit

# Inside, save the copied result to a salt key variable
salt: McQIX4LnIRhZ4BUHB9lRxnu++TnPMv2XLjJIxvzSJg8=

# Press esc key then type :wq to save and exit
# You may need to restart your rails server

‌Now we have a persisted salt variable which uses to encrypt and decrypt any string. Now, all we need to do is to include our shared method, crypt and use it:‌

class User < ApplicationRecord
  # Some basic validation when we call our save method only.
  # See more: https://guides.rubyonrails.org/v3.2/active_record_validations_callbacks.html
  validates :consumer_key, presence: true, on: :save_keys
  validates :consumer_secret, presence: true, on: :save_keys

  # This is how we reuse a shared method in our rails model (crypt)
  include CryptConcern

  # HACK! We use the user model as an example. Normally you would use another model
  # in a situation like this. When a new user is created or updated,
  # this callback will get triggered
  before_save :encrypted_consumer_key_and_secret,
    if: Proc.new { |user| !user.consumer_key.nil? }

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

    def encrypted_consumer_key_and_secret
      encrypted_key = crypt.encrypt_and_sign(self.consumer_key)
      encrypted_secret = crypt.encrypt_and_sign(self.consumer_secret)
      self.consumer_key = encrypted_key
      self.consumer_secret = encrypted_secret

‌There you have it. Very simple. Depending on your use case, you may/not use this method or use a more sophisticated gem.

Need to 👀 the repo? Say no more. Until next time, ✌🏼