Blog

Thoughts from my daily grind

Encrypting & decrypting sensitive data in Ruby on Rails

Posted by Ziyan Junaideen |Published: 21 October 2021 |Category: Ruby on Rails
Default Upload |

Cryptography is an integral part of modern software development. The most common use of encryption on a Ruby on Rails application would be to store sensitive data on a database table for later withdrawal. For that, you have the attr_encrypted gem. But there are times you need more control over the process. That is the intended audience of this blog post.

I have temporarily stored encrypted credentials in a Redis database with a short TTL for the following reasons.

  • Store credit cards and ACH details
  • Storing API credentials

Once I encrypt and store the details in an HTTP POST request, I will fire an ActiveJob/SideKiq/Resque process. For example, this process will use the credit card details to create a customer and store the card in USAePay service and update the user record with the returned ID.

Requirements

The Encryptor gem is my go-to tool to encrypt and decrypt data. I use it in conjunction with random keys, random initializing vectors (iv) generated using OpenSSL and a salt (a random byte sequence) generated using SecureRandom.

Install Encryptor gem using bundle add encryptor.

# Gemfile
gem 'encryptor', '~> 3.0.0'

Both OpenSSL and SecureRandom come with Ruby, and no additional setup is required.

Encryption

To encrypt data, we need to generate a few parameters, and we need them later to decrypt the encrypted data. These parameters are:

  • Random key
  • Initializing vector
  • Salt

Let's make a data structure to include these data as well as the encrypted payload.

module JDeen
  class Encrypted
    ENC_ATTRIBTUES = %i[key iv salt data].freeze
    attr_accessor *ENC_ATTRIBTUES
  end
end

Now lets write a method to encrypt:

module JDeen::Encryption
  def self.encrypt(payload)
    result = Encrypted.new

    cipher = OpenSSL::Cipher.new('aes-256-gcm')
    cipher.encrypt

    result.key = cipher.random_key
    result.iv = cipher.random_iv
    result.salt = SecureRandom.random_bytes(16)

    result.data = Encryptor.encrypt(
      algorithm: 'aes-256-gcm',
      value: payload,
      key: result.key,
      iv: result.iv,
      salt: result.salt
    )

    result
  end
end

With this method, you can call and retrieve one object containing the encrypted data + the parameters provided to encrypt data.

encrypted = JDeeen::Encryption.encrypt("Hello Encryption!")
encrypted.key  # =>  "\x9C\x99\xF2i\xC5\x9E!\x8B\x02\e\xB2N\x85\xB7\xD3\x9A{\x1AB/Jy)\xA1\xCD\e.\xB6\a\xA95L"
encrypted.iv   # => "\x7F)\xEA\xBA\xD4\x0ER1i\x12n\xBB"
encrypted.salt # => "*[B\r\xDB`\x8FCL\xD1W\x83\xB3U\e\xEB"
encrypted.data # => "\x03\x83\xFA%\xE2\xC2\x16\xE4eR\xCF\x81\x14\e\xB0!\xF3\x8ER\xD6]~;\x1A"}

Note: Different encryption algorithms have a different secret_key, iv, and salt requirements. OpenSSL handles the requirements for secret_key and iv. For aes-256-gcm we need a 16 byte salt.

Note: Unlike Encryptor version 1.x, versions 2.x and 3.x strictly validate paremeter requirements.

Decryption

Now that we encrypted the data, we need to decrypt it when we want. Let's extend the JDeen::Encryption module with a method to decrypt.

module JDeen::Encryption
  # ...

  def self.decrypt(result)
    return unless result.decryptable?

    decrypted_value = Encryptor.decrypt(
      algorithm: 'aes-256-gcm',
      value: result.data,
      key: result.key,
      iv: result.iv,
      salt: result.salt
    )

    decrypted_value
  end
end

This method accepts the output of the encrypt(payload) method and returns the decrypted value.

encrypted = JDeen::Encryption.encrypt("Hello Encryption!")
decrypted = JDeen::Encryption.decrypt(encrypted)
decrypted # => "Hello Encryption!"

Extra: You would have noticed result.decryptable? method call. That is introduced to assure that we have the attributes to process the decryption. The example I copied to this post included storing the encryption attributes in a Redis DB with a TTL of 5 minutes. So when the app triggered the decryption, there was a chance the data wasn't there.

module JDeen::Encryption
  class Encrypted
      # ...

      def decryptable? 
        ENC_ATTRIBTUES.each do |attribute|
          value = send(attribute)
          return false if value.nil? || value.empty?
        end
        true
    end
  end
end

Note: Notice I have used value.nil? || value.empty?. The RSpec I wrote randomly bugged out when using present? on values generated for secure_key, iv, and salt. I am not sure if it was some sort of a bug in Ruby 2.4.4, from which the code originated.

Conclusion

Here we discussed how to encrypt and decrypt a value. If you want to store the values temporarily (ex: until we process credit card information in a background job), you can store the data in a Redis DB with a TTL. I originally intended to cover storing the data in PG and Redis and later decided to write a separate blog post since this is already long.

Tags
About the Author

Ziyan Junaideen -

Ziyan is an expert Ruby on Rails web developer with 8 years of experience specializing in SaaS applications. He spends his free time he writes blogs, drawing on his iPad, shoots photos.

Comments