TLDR: Rails 7 Active Record Encryption
PII (personal identifiable information) and PHI (personal health information) is considered very sensitive, and if you are processing it, there can be requirements to protect it.
Rails 7 added Active Record Encryption, that replaces gems like attr_encrypted. It is a long-awaited default feature by organizations that have high data-security standarts and requirements.
Why you need attribute encryption:
- If someone steals your database dump, they won’t have easy access to the encrypted attributes (they will also need the encryption keys)
- Encrypted columns are automatically filtered in logs
Security-oriented static code analysis tools like bearer/bearer can hint on what attributes you should encrypt:
1. Encrypt attributes
If you run this command in the console
bin/rails db:encryption:init
it will generate a few lines, that you should add to credentials.yml
:
active_record_encryption:
primary_key: 1qRx9LKs1ON5gbk0q5Affs898O0S0sXo
deterministic_key: pCgz9AgTkwO8zcn3hrZBL6tbNVQyxGvL
key_derivation_salt: pOIy8FEWO3hVpt1f05LKuWETU1uOICPb
Try to encrypt attributes:
# app/models/client.rb
class Client < ApplicationRecord
encrypts :name, deterministic: true, downcase: true # string
encrypts :annual_income # integer
encrypts :date_of_birth # datetime
encrypts :health_condition # text
has_rich_text :description, encrypted: true # ActionText
end
2. Problems that you will encounter:
2.1. Encryptable data types:
You can really encrypt only string
and text
fields (because we store a long hashed string on the database level).
It is recommended to store encryptable attributes as text
, not string
.
If you want to encrypt an integer
or datetime
, you will get errors. You have to store encryptable data as text
.
I think it is quite safe to change column type from integer to text. If you do so, further encryption will be easy.
class StoreIntegersAsText < ActiveRecord::Migration[7.0]
def change
change_column :clients, :annual_income, :text
end
end
2.2. Querying encrypted data:
Only if you use deterministic encryption, you will be able to query the database, but only for an exact match like Client.find_by(username: 'yarotheslav')
.
2.3. Encrypting existing data
If you want to add encryption to existing attributes that already store data, you will get an error.
Add these lines to application.rb
to allow encrypted and unencrypted data to co-exist:
# config/application.rb
config.active_record.encryption.support_unencrypted_data = true
config.active_record.encryption.extend_queries = true
3. Useful commands
client = Client.last
# encrypt all attributes that use encrypts
client.encrypt
# decrypt all attributes that use encrypts
client.decrypt
# get the value that is stored in the database, not the decrypted version
client.ciphertext_for :sexual_orientation
# is this attribute encrypted?
client.encrypted_attribute? :sexual_orientation
# encrypt all records
Client.all.map(&:encrypt)
# decrypt all records
Client.all.map(&:decrypt)
Example:
Summary:
- Encrypting attributes adds a layer of complexity, but sometimes external factors force you to do it.
- I currently use Active Record Encryption to encrypt
ApiToken.secret_token
in an app.