Rails 7 introduced in-built encryption called Active Record Encryption and attr_encrypted has reached its expiry. And now many projects are on the verge of moving from attr_encrypted to Active Record Encryption. This blog will explain how this migration can be made possible with very few steps.
Data in attr_encrypted and Active Record Encryption:
attr_encrypted uses two columns in the DB to save the data, the encrypted_attribute
and encrypted_attribute_iv
. Active Record Encryption uses a single column same as the name of the attribute and saves the data as a hash. For example, to encrypt address
in the User
model, with attr_encrypted, there are two columns encrypted_address
and encrypted_address_iv
in DB. With active record encryption, there is a single column in DB, address
where the data is saved as a hash as below
{"p":"Hashed Value","h":{"iv":"Hashed Value","at":"Hashed Value"}}
As an example, the article will migrate the address
attribute from the User
model. The migration includes the following steps,
Add Active Record Encryption to the Rails 7 app.
Add
address
andaddress_tmp
columns to DBCreate migration to fill data into columns
Remove the attr_encrypted gem and drop columns
Step 1: Add Active Record Encryption to the Rails 7 app
The new encryption from Rails 7 uses 3 keys, primary_key
, deterministic_key
, and key_derivation_salt
. Generate them using the following command
bin/rails db:encryption:init
Now save them in your application.rb
config.active_record.encryption.primary_key = "Primary Key"
config.active_record.encryption.deterministic_key = "Deterministic Key"
config.active_record.encryption.key_derivation_salt = "Key Derivation Salt"
Step 2: Add address
and address_tmp
columns to DB
As mentioned in the introduction, attr_encrypted uses different columns in DB and uses methods to return the value of the address
attribute. So the next step is to introduce the column with the same name as the attribute in the DB. But before that, ignore the column in the model.
Class User < ApplicationRecord
self.ignored_columns = %w[address]
end
Now create a migration to add the column in the User Table. The data can’t be moved directly to the address
column since it's still used by attr_encrypted, so create two columns in DB. address
and address_tmp
. address_tmp
will be the temporary column to save the encrypted value. The data is first saved in address_tmp
and then the address
column is updated using the update_all
command. The update_all
command does not instantiate the User
model or trigger Active Record callbacks and validations, which works perfectly in the current scenario.
class AddAddressAndTmpColumnToUser < ActiveRecord::Migration[7.0]
def change
add_column :users, :address, :text
add_column :users, :address_tmp, :text
end
end
Now add the encrypts command on the User
model from Active Record Encryption which does all the magic. The encrypts point to the address_tmp
and not address
, because the attr_encrypted gem still uses the address
method.
encrypts :address_tmp
Step 3: Create migration to fill data into columns
Create a migration and fill in values into the address_tmp
column and then to the address
column using update_all
. The code that goes into the migration,
User.find_in_batches do |users_batch|
users_batch.map do |user|
user.address_tmp = user.address
user.save
end
end
User.update_all("address=address_tmp")
Also, add a temporary code in the User
model which will save the value in the address
and address_tmp
columns during updates made by clients using your application.
after_commit :write_address, if: -> { previous_changes[:address] }
def write_address
self.address_tmp = address
save!
User.where(id: id).update_all("address=address_tmp")
end
Now the data is migrated to the address
and address_tmp
columns. Any updates made by clients are saved as well in the new columns.
Step 4: Remove the attr_encrypted gem and drop columns
Remove the attr_encrypted reference from the code before removing the gem completely. The existing code might look something like the one below
attr_encrypted :address, key: "key"
While removing the above code, also point to the new column address
with the encrypted data. In the User model add the following code,
encrypts :address
Remove the references that were temporarily created in the User
model,
# to be removed from User model
self.ignored_columns = %w[address]
after_commit :write_address, if: -> { previous_changes[:address] }
encrypts :address_tmp
Also if you are using encrypted_address
or encrypted_address_iv
directly in your code anywhere, make sure to replace them with the new values from the address
attribute. Once all the references are removed, it's safe to remove the gem attr_encrypted from the code.
The final step is to remove the following columns encrypted_address
, encrypted_address_iv
, and address_tmp
from User
model.
To know more details on Active Record Encryption, here is the official link https://guides.rubyonrails.org/active_record_encryption.html