升级到 Rails 7 时迁移加密的数据库字段

CWa*_*ton 5 encryption ruby-on-rails lockbox-3

问题

由于这个问题,我不得不将我的 RoR 应用程序升级到 Rails 7 。进行此升级时,由于 Rails 使用本机解密尝试解密字段,因此无法再读取使用Lockbox gem加密的 db 列。我在 GitHub 上将它作为一个问题发布,但我也想知道是否有其他人有解决方案将数据从一种加密格式迁移到新的本机加密中,该加密将随 Rails 7.0 一起提供(目前 Rails 的稳定版本是6.1.4 和 Rails 7.0.alpha 在 GitHub 的主分支上)

代码

应用程序/模型/journal_entry.rb

class JournalEntry < ApplicationRecord
  belongs_to :prayer_journal

  encrypts  :content
  validates :content, presence: true  
end
Run Code Online (Sandbox Code Playgroud)

数据库/模式.rb

create_table "journal_entries", force: :cascade do |t|
    t.bigint "prayer_journal_id", null: false
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.text "content_ciphertext"
    t.index ["prayer_journal_id"], name: "index_journal_entries_on_prayer_journal_id"
  end
Run Code Online (Sandbox Code Playgroud)

第一个日志条目的控制台输出

#<JournalEntry:0x00007f95364745c8
 id: 1,
 prayer_journal_id: 1,
 created_at: Sat, 15 May 2021 00:00:00.000000000 UTC +00:00,
 updated_at: Sat, 17 Jul 2021 03:12:34.951395000 UTC +00:00,
 content_ciphertext: "l6lfumUqk9RqUHMf0aVUfL2sL+WqkhBmHpyqKqMtxD4=",
 content: nil>
Run Code Online (Sandbox Code Playgroud)

CWa*_*ton 10

经过几个小时的阅读 Rails 指南和各种讨论新的本机加密的博客文章后,我能够弄清楚如何迁移数据。这是一个多步骤的过程,但我觉得我将其放在这里以供将来对其他人的帮助。

首先,我确实想说,如果我正确阅读指南,可能会列出其他加密/解密提供商。我无法弄清楚这一点,因此决定使用我所知道的来创建解决方案。

我是如何想出解决方案的

我注意到在我的架构中实际上没有“内容”列,而是一个“content_ciphertext”列,当在 中调用密码箱时encrypt :content,它会加密并将其放置在该列中。我可以打电话JournalEntry.first.content让它解密该content_ciphertext字段并提供纯文本。这就是为什么升级到Rails 7和本机加密后,它一直说该列contentnil;因为实际上并没有这个名字的专栏。Rails 7 在模式中使用精确的命名,而不是在列名后附加“密文”等。

有了这些知识,我就解决了剩下的问题。

解决步骤

  1. 升级 RAILS 版本之前:创建迁移以将内容字段添加到包含加密数据的表中。就我而言,那里有三张桌子。所以,我运行了这段代码:rails g migration AddUnencryptedContentFieldToDatabaseTabels

并将迁移文件更改为如下所示:

# db/migrate/*******_add_unencrypted_content_field_to_database_tabels.rb

class AddUnencryptedContentFieldToDatabaseTabels < ActiveRecord::Migration[6.1]
  def up
    add_column    :journal_entries,        :unencrypted_content,         :text
    add_column    :prayer_requests,        :unencrypted_content,         :text
    add_column    :prayer_request_updates, :unencrypted_content,         :text
  end

  def down
    remove_column :journal_entries,        :unencrypted_content
    remove_column :prayer_requests,        :unencrypted_content
    remove_column :prayer_request_updates, :unencrypted_content
  end
end
Run Code Online (Sandbox Code Playgroud)

完成后,我编写了一个 rake 任务来遍历所有加密字段并将其复制到未加密的列中。

# lib/tasks/switch_encryption_1.rake

desc 'This goes through and copies encrypted data to a non-encrypted field to start the process of migrating to new native encryption.'
task :switch_encryption_1 => :environment do
    puts "Journal Entries where content needs to be unencrypted: " + JournalEntry.where(unencrypted_content:nil).count.to_s
    JournalEntry.where(unencrypted_content:nil).each do |j|
        j.update(unencrypted_content:j.content)
    end
    puts "Journal Entries where content needs to be unencrypted after code run: " + JournalEntry.where(unencrypted_content:nil).count.to_s

    puts "Prayer Requests where content needs to be unencrypted: " + PrayerRequest.where(unencrypted_content:nil).count.to_s
    PrayerRequest.where(unencrypted_content:nil).each do |r|
        r.update(unencrypted_content:r.content)
    end
    puts "Prayer Requests where content needs to be unencrypted after code run: " + PrayerRequest.where(unencrypted_content:nil).count.to_s

    puts "Prayer Request Updates where content needs to be unencrypted: " + PrayerRequestUpdate.where(unencrypted_content:nil).count.to_s
    PrayerRequestUpdate.where(unencrypted_content:nil).each do |u|
        u.update(unencrypted_content:u.content)
    end
    puts "Prayer Request Updates where content needs to be unencrypted after code run: " + PrayerRequestUpdate.where(unencrypted_content:nil).count.to_s
end
Run Code Online (Sandbox Code Playgroud)

这两篇文章都写好了,我现在可以将代码部署到生产中。部署后,我将rake db:migrate在生产控制台中运行,然后rake switch_encryption_1遍历并解密所有字段并将其复制到新列。

然后,我还可以进行测试,以确保数据在继续之前确实被复制和解密。

  1. 回到开发中,我现在可以更新我的Gemfile新 Rails 主分支,因为我已经解密了字段。所以,我将其更改Gemfile为:

    gem 'rails', :github => 'rails/rails', :branch => 'main'

然后,您需要通过bin/rails db:encryption:init在控制台中运行并将值复制到凭据文件来创建加密密钥。如果您不知道如何执行此操作,请运行以下代码EDITOR=nano rails credentials:edit并将值复制到该文件中:

active_record_encryption:
  primary_key: xxxxxxxxxxxxxxxxxxx
  deterministic_key: xxxxxxxxxxxxxxxxxxx
  key_derivation_salt: xxxxxxxxxxxxxxxxxxx
Run Code Online (Sandbox Code Playgroud)

然后按照提示保存并退出。对我来说,这是 Control + 大写字母“O”来写出,然后 Control + 大写字母“X”来退出。这将有利于发展。从 Rails 6 开始,我们已经能够为不同的环境设置不同的凭据。因此,您可以复制相同的数据,但在控制台中运行EDITOR=nano rails credentials:edit --environment production以获取生产凭据。(记住要保证这些密钥的安全,并且不要将它们签入版本控制)

然后我创建了另一个迁移rails g migration AddContentFieldToDatabaseTabels

并将迁移文件更改为如下所示:

# db/migrate/*******_add_content_field_to_database_tabels.rb

class AddContentFieldToDatabaseTabels < ActiveRecord::Migration[6.1]
  def up
    add_column    :journal_entries,        :content,             :text
    add_column    :prayer_requests,        :content,             :text
    add_column    :prayer_request_updates, :content,             :text

    remove_column :journal_entries,        :content_ciphertext
    remove_column :prayer_requests,        :content_ciphertext
    remove_column :prayer_request_updates, :content_ciphertext
  end

  def down
    remove_column :journal_entries,        :content
    remove_column :prayer_requests,        :content
    remove_column :prayer_request_updates, :content

    add_column    :journal_entries,        :content_ciphertext,  :text
    add_column    :prayer_requests,        :content_ciphertext,  :text
    add_column    :prayer_request_updates, :content_ciphertext,  :text
  end
end
Run Code Online (Sandbox Code Playgroud)

您可能会注意到,我还添加了代码来删除旧的加密列。这是因为它将不再使用,并且我已经验证内容现在已保存在列中unencrypted_content

然后,我编写了另一个 rake 任务来遍历并将所有数据从unencrypted_content列复制到content列。由于我的模型已经有了之前使用 Lockbox gem 的代码encrypts :content,因此我不需要将其添加到模型中以让 Rails 知道要加密这些列。

# lib/tasks/switch_encryption_2.rake

desc 'This goes through and encrypts the unencrypted data and copies it to the encrypted field to finish migrating to new native encryption.'
task :switch_encryption_2 => :environment do
    JournalEntry.all.each do |j|
        j.update(content:j.unencrypted_content)
    end

    PrayerRequest.all.each do |r|
        r.update(content:r.unencrypted_content)
    end

    PrayerRequestUpdate.all.each do |u|
        u.update(content:u.unencrypted_content)
    end

    puts "Finished Encrypting"
end
Run Code Online (Sandbox Code Playgroud)

现在,部署。您的生产凭据也应该已部署用于加密。现在在生产控制台中运行它:rake db:migraterake switch_encryption_2。完成此操作后,我验证了加密是否有效。

  1. 我现在可以在开发中创建另一个迁移来删除未加​​密的表列。就像这样:rails g migration DeleteUnencryptedContentFieldFromDatabaseTables

db/migrate/********_delete_unencrypted_content_field_to_database_tabels.rb

class DeleteUnencryptedContentFieldToDatabaseTabels < ActiveRecord::Migration[6.1]
    def up
        remove_column :journal_entries,        :unencrypted_content
        remove_column :prayer_requests,        :unencrypted_content
        remove_column :prayer_request_updates, :unencrypted_content
    end

    def down
        add_column    :journal_entries,        :unencrypted_content,  :text
        add_column    :prayer_requests,        :unencrypted_content,  :text 
        add_column    :prayer_request_updates, :unencrypted_content,  :text
    end
end
Run Code Online (Sandbox Code Playgroud)

将其推入生产并运行rake db:migrate

此时,一切都应该迁移到新的本机 Rails 7 加密。

我希望这对未来的程序员有所帮助。快乐编码!

奖金部分

对于我们当中那些偏执的人,或者处理非常敏感的数据并需要确保未加密的列不再存在的人。这是我创建的第三个 rake 任务,它会遍历并使用nil. 您可以在部署迁移之前运行此命令以删除列。但实际上,这可能有点矫枉过正:

desc 'After verifying that the data is now encrypted and able to be decrypted, this task will go through and erase the unencrypted fields'
task :switch_encryption_3 => :environment do
    JournalEntry.all.each do |j|
        j.update(unencrypted_content:nil)
    end

    PrayerRequest.all.each do |r|
        r.update(unencrypted_content:nil)
    end

    PrayerRequestUpdate.all.each do |u|
        u.update(unencrypted_content:nil)
    end

    puts "Finished Enrasing Unencrypted Data. You will need to run a new migration to delete the 'unencrypted_content' fields."
end
Run Code Online (Sandbox Code Playgroud)