Skip to content

Migrating your data from v1.x to v3.x format

Steven Chanin edited this page Sep 15, 2016 · 2 revisions

While the insecure_mode described above will keep you operating short term, what you really want is to move forward to the newer, more secure settings that v3.x makes available. However, if you have a production system that has data you care about saved away using 1.x encryption, you need get that data converted.

After updating the gem as described above, we are going to do that using the following logical steps:

  1. Rename the existing DB columns that have encrypted data (so we have access to it during the conversion)
  2. Add the new columns needed for v3.x format encryption to the database
  3. Update your model so it can access the old format data and the new format data
  4. Go through all the rows decrypting the old data and re-encrypting / re-saving it in v3.x format
  5. Verify that the old and new data is the same
  6. Update your model to remove the temporary code that accessed the old data
  7. Remove the columns in the DB that hold the old, outdated encrypted values

Our example

For the purposes of this example, let's assume that we have User that have account_number which is encrypted. So that means, that in the users table, there is a column encrypted_account_number.

and in the models/user.rb we have

attr_encrypted :account_number, :key => 'ReallyLongKeyWithLotsOfRandomness'

Steps 1 & 2 - Column Renaming

generate a migration to create the save the old values and add new columns

rails g migration add_iv_to_users encrypted_account_name_iv:string

using that as a starting point, change the def change to a two methods def up and def down so you can explicitly control the order in which things are changed.

class AddIvToUsers < ActiveRecord::Migration
  def up
    rename_column :users, :encrypted_account_number, :encrypted_account_number_old

    add_column :users, :encrypted_account_number, :string
    add_column :users, :encrypted_account_number_iv, :string
  end

  def down
    remove_column :users, :encrypted_account_number, :string
    remove_column :users, :encrypted_account_number_iv, :string

    rename_column :users, :encrypted_account_number_old, :encrypted_account_number
  end
end

Step 3 - Update the model

We need to change the model so that it uses insecure mode to access the _old value and the new v3.x settings for the new columns.

in models/user.rb change things so:

  attr_encrypted :account_number, :key => 'ReallyLongKeyWithLotsOfRandomness'

  attr_encrypted :account_number_old, :key => 'ReallyLongKeyWithLotsOfRandomness', algorithm: 'aes-256-cbc', mode: :single_iv_and_salt, insecure_mode: true

Test that you can access the old v1.x values in the console

$ rails console

2.3.0 :001 > u = User.first
2.3.0 :002 > u.account_number_old
 => "123456"

Test that you can save encrypted values in the new format

2.3.0 :003 > u.account_number = u.account_number_old
 => "123456"
2.3.0 :004 > u.save
 => true
2.3.0 :005 > u.reload
2.3.0 :006 > u.account_number
 => "123456"

Steps 4 & 5 - Migrate the data and verify it

create a new file lib/tasks/encrypt.rake with 2 tasks

namespace :encrypt do
  desc "Migrate user info to new encryption format"
  task :user => :environment do
    updated_count = 0
    error_count = 0

    User.find_each do |user|
      user.account_number = user.account_number_old

      if user.save
        updated_count += 1
      else
        puts "** Error while updating ID: #{user.id}"
        error_count += 1
      end
    end

    puts "Update complete. Total rows in table: #{User.count}"
    puts "Updated #{updated_count} record(s). Hit #{error_count} error(s)."
  end

  desc "Verify update was successful"
  task :verify_user => :environment do
    verified_count = 0
    error_count = 0

    User.find_each do |user|
      if user.account_number == user.account_number_old
        verified_count += 1
      else
        puts "** Error values did not match for ID: #{user.id}"
        error_count += 1
      end
    end

    if verified_count == User.count
      puts "All #{verified_count} row(s) match."
    else
      puts "ERROR -- #{error_count} row(s) do not match"
    end
  end
end

Run this code against a copy of your database & make sure everything runs without error. Then you'll need to run this task against your production data.

Step 6 - Take reference to old values out of your model

Once you've successfully updated your production data, you should remove the line:

  attr_encrypted :account_number_old, :key => 'ReallyLongKeyWithLotsOfRandomness', algorithm: 'aes-256-cbc', mode: :single_iv_and_salt, insecure_mode: true

from your models/user.rb

Step 7 - Drop the columns with old values

Create a new migration

rails g migration remove_old_encrypted_values_from_user

this should look something like:

class RemoveOldEncryptedValuesFromUser < ActiveRecord::Migration
  def up
    remove_column :users, :encrypted_account_number_old, :string
  end

  def down
    add_column :users, :encrypted_account_number_old, :string
    puts "**** WARNING: you've just rolled and you have a column for the old value, but it's empty. ***"
  end
end