Encrypting & decrypting sensitive data in Ruby on Rails
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.
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.