Hybrid Cryptography on Rails
Hybrid cryptography allows certain servers to encrypt data without the ability to decrypt it. This can greatly limit damage in the event of a breach.
Suppose we have a service that sends text messages to users. Users enter their phone number through the website or mobile app.
With hybrid cryptography, we can set up web servers to only encrypt phone numbers. Text messages can be sent through background jobs which run on a different set of servers - ones that can decrypt and don’t allow inbound traffic. If admins need to view phone numbers, they can use a separate set of web servers that are only accessible through the company VPN.
|User web servers||✓|
|Background workers||✓||✓||No inbound traffic|
|Admin web servers||✓||✓||Requires VPN|
Install Libsodium and add Lockbox and RbNaCl to your Gemfile:
gem 'lockbox' gem 'rbnacl'
Generate keys in the Rails console with:
Store the keys with your other secrets. This is typically Rails credentials or an environment variable (dotenv is great for this). Be sure to use different keys in development and production.
Only set the decryption key on servers that should be able to decrypt.
We’ll store phone numbers in an encrypted database field. Add to your Gemfile:
Create a migration to add a new column for the encrypted data. We don’t need a separate IV column, as this will be included in the encrypted data.
class AddEncryptedPhoneToUsers < ActiveRecord::Migration[5.2] def change add_column :users, :encrypted_phone, :string end end
In the model, use a custom encryptor provided by Lockbox.
class User < ApplicationRecord attr_encrypted :phone, encryptor: Lockbox::Encryptor, algorithm: "hybrid", encryption_key: ENV["PHONE_ENCRYPTION_KEY"], decryption_key: ENV["PHONE_DECRYPTION_KEY"] attr_accessor :encrypted_phone_iv # prevent attr_encrypted error end
Set a user’s phone number to ensure it works.
Suppose we also need to accept sensitive documents. We can take a similar approach with file uploads.
For CarrierWave, use:
class DocumentUploader < CarrierWave::Uploader::Base encrypt algorithm: "hybrid", encryption_key: ENV["PHONE_ENCRYPTION_KEY"], decryption_key: ENV["PHONE_DECRYPTION_KEY"] end
For Active Storage, use:
class User < ApplicationRecord attached_encrypted :document, algorithm: "hybrid", encryption_key: ENV["PHONE_ENCRYPTION_KEY"], decryption_key: ENV["PHONE_DECRYPTION_KEY"] end
You can also encrypt an IO stream directly.
box = Lockbox.new(algorithm: "hybrid", encryption_key: ENV["PHONE_ENCRYPTION_KEY"], decryption_key: ENV["PHONE_DECRYPTION_KEY"]) box.encrypt(params[:file])
You’ve now seen an approach for keeping your data safe in the event a server is compromised. For more on data protection, check out Securing Sensitive Data in Rails.