问题描述
问题
由于 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",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>
解决方法
在阅读 Rails 指南和各种谈论新的原生加密的博客文章几个小时后,我能够弄清楚如何迁移数据。这是一个多步骤的过程,但我觉得我会把它放在这里以供将来帮助他人。
首先,我确实想说,如果我正确阅读 the guides,可能会列出其他加密/解密提供程序。我无法弄清楚这一点,因此决定使用我所知道的来创建一个解决方案。
我是如何想出解决方案的
我注意到在我的架构中实际上没有“内容”列,而是“内容密文”列,当在 encrypt :content
中调用密码箱时,它会加密并将其放置在该列中。我可以调用 JournalEntry.first.content
让它解密 content_ciphertext
字段并提供纯文本。这就是为什么升级到 Rails 7 和原生加密后,它一直说 content
列是 nil
;因为实际上,没有该名称的列。 Rails 7 使用模式中的确切命名,而不是在列名后附加“密文”等。
拥有这些知识为我解决了其余的问题。
解决步骤
- 升级 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,:text
add_column :prayer_request_updates,: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
以遍历并解密所有字段并将其复制到新列。
然后我还可以进行测试以确保在继续之前确实复制和解密了数据。
-
回到开发阶段,我现在可以更新我的
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
然后按照提示保存退出。对我来说,这是 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
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
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:migrate
和 rake switch_encryption_2
。在我这样做之后,我确认加密有效。
- 我现在可以在开发中创建另一个迁移来删除未加密的表列。像这样:
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,:text
add_column :prayer_requests,:text
add_column :prayer_request_updates,: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