【问题标题】:Migrating encrypted database fields when upgrading to Rails 7升级到 Rails 7 时迁移加密的数据库字段
【发布时间】:2021-07-17 05:45:16
【问题描述】:

问题

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

代码

app/models/journal_entry.rb

class JournalEntry < ApplicationRecord
  belongs_to :prayer_journal

  encrypts  :content
  validates :content, presence: true  
end

db/schema.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

第一个日记帐分录的控制台输出

#<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>

【问题讨论】:

    标签: ruby-on-rails encryption lockbox-3


    【解决方案1】:

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

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

    我是如何想出解决方案的

    我注意到在我的架构中实际上没有“内容”列,而是“内容密文”列,当在 encrypt :content 中调用密码箱时,它会加密并将其放置在该列中。我可以打电话给JournalEntry.first.content 让它解密content_ciphertext 字段并提供纯文本。这就是为什么在升级到 Rails 7 和原生加密之后,它一直说 content 列是 nil;因为实际上,没有该名称的列。 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
    

    完成后,我编写了一个 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
    

    这两个都写好了,我现在可以将代码部署到生产环境中。部署后,我将在生产控制台中运行 rake db:migrate,然后运行 ​​rake switch_encryption_1 来完成并解密并将所有字段复制到新列中。

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

    1. 回到开发阶段,我现在可以更新我的 Gemfile 新的 Rails 主分支,因为我已经解密了这些字段。所以,我把Gemfile改成这样:

      gem 'rails', :github =&gt; 'rails/rails', :branch =&gt; 'main'

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

    active_record_encryption:
      primary_key: xxxxxxxxxxxxxxxxxxx
      deterministic_key: xxxxxxxxxxxxxxxxxxx
      key_derivation_salt: xxxxxxxxxxxxxxxxxxx
    

    然后按照提示保存退出。对我来说,就是 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
    

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

    现在,部署。您的生产凭据也应该已部署用于加密。现在在生产控制台中运行它: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
    

    将其推送到生产环境并运行 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
    

    【讨论】:

    • 在运行switch_encryption_1.rake 之前这里缺少一个步骤。如果content_ciphertext 列发生变化,您需要在控制器或模型中添加一些逻辑来更新unencrypted_content 列。如果用户在 rake 任务运行时更新记录并且它已经处理了给定的记录,则这是必要的。从使用unencrypted_content 迁移到content 时需要使用相同的方法。
    • 感谢@AbM 的建议。我没有添加它,因为每当我需要进行这样的数据库更改时,我都会通知我的用户计划维护,然后我将我的应用程序置于维护模式,这样任何人都无法进行更改。这只是我的工作流程,因此不需要控制器代码。但是,其他人肯定需要这样做。
    猜你喜欢
    • 2016-04-09
    • 2013-05-11
    • 1970-01-01
    • 2018-02-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-28
    • 1970-01-01
    相关资源
    最近更新 更多