🕵️‍♂️

【Rails】Active Record暗号化を試してみた

2024/12/23に公開

はじめに

お疲れ様です。
おおくまです。

今回は、「【Rails】Active Record暗号化を試してみた」ということで、Active Record暗号化についてまとめてみました。

少しでも皆様の参考になりますと幸いです。

対象読者

注意点

環境

Active Record暗号化とは

Active Record暗号化とは、その名の通り、Active Recordを使用してデータベースに保存するデータを暗号化することです。
暗号化する属性を宣言することで、Active Recordが暗号化と復号をサポートしてくれます。

Active Record暗号化を使用する理由としては、データベースに保存されるデータが暗号化されるため、仮に攻撃者がデータベースにアクセスしたとしても、データを読み取ることができないため、データの安全性を高めることができます。

GMOインターネットグループ:暗号化とは?主な方式の種類や暗号化・復号化の仕組み、やり方について解説より引用

Active Record暗号化を試してみる

前提

今回は、Articleテーブルのbodyカラムを暗号化してみます。
以下のようなArticleテーブルを作成しました。

app/models/article.rb
class Article < ApplicationRecord
  validates :title, presence: true, length: { maximum: 255 }
  validates :body, presence: true, length: { maximum: 65_535 }
end
db/schema.rb
create_table "articles", force: :cascade do |t|
  t.string "title", null: false
  t.string "body", null: false
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
end

暗号化の設定をする前に、Articleレコードを作成してみます。

docker compose exec app rails c
Loading development environment (Rails 7.0.8.6)

irb(main):001> Article.create(title:"title", body:"body")
  TRANSACTION (0.1ms)  BEGIN /*application:TestApp*/
  Article Create (1.7ms)  INSERT INTO `articles` (`title`, `body`, `created_at`, `updated_at`) VALUES ('title', 'body', '2024-12-22 11:47:02.622943', '2024-12-22 11:47:02.622943') /*application:TestApp*/
  TRANSACTION (2.6ms)  COMMIT /*application:TestApp*/
=>
#<Article:0x0000ffff65cf4620
 id: 1,
 title: "title",
 body: "body",
 created_at: Sun, 22 Dec 2024 20:47:02.622943000 JST +09:00,
 updated_at: Sun, 22 Dec 2024 20:47:02.622943000 JST +09:00>

データベースでArticleレコードを確認してみます。

docker compose exec db mysql -u root -p

mysql> USE test_app_development;
Database changed

mysql> SELECT * FROM articles\G;
*************************** 1. row ***************************
        id: 1
     title: title
      body: body
created_at: 2024-12-22 11:47:02.622943
updated_at: 2024-12-22 11:47:02.622943
1 row in set (0.00 sec)

Articleレコードが作成されており、値が暗号化されていないことが確認できました。

では、実際にArticleテーブルのbodyカラムを暗号化してみます。

Active Record暗号化の設定

まずは、Active Record暗号化を使用するための設定を行います。

docker compose exec app bin/rails db:encryption:init
Add this entry to the credentials of the target environment:

active_record_encryption:
  primary_key: tVZdJ1FxwuzLMjS5yLB3cnGlJicXYIcR
  deterministic_key: UTx3Az9dnwOav4RgHpfj3IhMDbn3kFi3
  key_derivation_salt: ChRRKgQSvEdHF2u9Dn2Qpy3LKFmVgWpz

上記のコマンドを実行すると、Active Record暗号化で使用するためのキーが発行されます。

では、これらのキーをRails credentialsに追加します。

docker compose exec -e EDITOR="vim" app bin/rails credentials:edit

上記のコマンドを実行すると、vimRails credentialsが開かれるので、キーを追加し、保存します。

docker compose exec app bin/rails credentials:show
# aws:
#   access_key_id: 123
#   secret_access_key: 345

# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: 21fa33728ddd1f36b6ea28bd9bb18e8c034ca0169ce63680baefa5a2f57786fbdfae6ef9a90f0348b57b347407091ee127807b8656cbbe73682b1692dc3ad633
active_record_encryption:
  primary_key: tVZdJ1FxwuzLMjS5yLB3cnGlJicXYIcR
  deterministic_key: UTx3Az9dnwOav4RgHpfj3IhMDbn3kFi3
  key_derivation_salt: ChRRKgQSvEdHF2u9Dn2Qpy3LKFmVgWpz

上記のコマンドを実行し、Rails credentialsにキーが追加されていることが確認できればOKです。

次にArticleのモデルファイルで、暗号化するカラムを設定します。
今回は、bodyカラムを暗号化するため、以下のように設定します。

app/models/article.rb
class Article < ApplicationRecord
+ encrypts :body

  validates :title, presence: true, length: { maximum: 255 }
  validates :body, presence: true, length: { maximum: 65_535 }
end

これで、Articleテーブルのbodyカラムが暗号化されるようになりました。
実際にArticleレコードを作成してみます。

暗号化されたデータを作成

docker compose exec app rails c
Loading development environment (Rails 7.0.8.6)

irb(main):001> Article.create(title:"title", body:"body")
  TRANSACTION (0.2ms)  BEGIN /*application:TestApp*/
  Article Create (1.6ms)  INSERT INTO `articles` (`title`, `body`, `created_at`, `updated_at`) VALUES ('title', '{\"p\":\"a1rcfg==\",\"h\":{\"iv\":\"CybdD1MAo/2fxlYl\",\"at\":\"UCbsKHRL8/8jmrw6snAYSQ==\"}}', '2024-12-22 11:50:58.009958', '2024-12-22 11:50:58.009958') /*application:TestApp*/
  TRANSACTION (2.4ms)  COMMIT /*application:TestApp*/
=>
#<Article:0x0000ffff79761640
 id: 2,
 title: "title",
 body: "body",
 created_at: Sun, 22 Dec 2024 20:50:58.009958000 JST +09:00,
 updated_at: Sun, 22 Dec 2024 20:50:58.009958000 JST +09:00>

発行されたSQLが前回と異なることが確認できます。
では、データベースでArticleレコードを確認してみます。

docker compose exec db mysql -u root -p

mysql> USE test_app_development;
Database changed

mysql> SELECT * FROM articles\G;
*************************** 1. row ***************************
        id: 1
     title: title
      body: body
created_at: 2024-12-22 11:47:02.622943
updated_at: 2024-12-22 11:47:02.622943
*************************** 2. row ***************************
        id: 2
     title: title
      body: {"p":"a1rcfg==","h":{"iv":"CybdD1MAo/2fxlYl","at":"UCbsKHRL8/8jmrw6snAYSQ=="}}
created_at: 2024-12-22 11:50:58.009958
updated_at: 2024-12-22 11:50:58.009958
2 rows in set (0.00 sec)

データベースに保存されているbodyカラムが暗号化されていることが確認できました。

非暗号化データを取得する

テーブルを作成した段階で、暗号化するよう設定している場合は、特に問題ありませんが、もう既にレコードが存在している段階で、途中から暗号化の設定を行った場合は、非暗号化データと暗号化済みデータが混在することになります。
暗号化の設定を行っている状態で、非暗号化データを取得しようとすると、Active Recordが非暗号化データを復号しようとするため、エラーが発生します。

現状、ID=1のArticleレコードは非暗号化データ、ID=2のArticleレコードは暗号化済みデータとなっています。
試しに、ID=1のArticleレコードと、ID=2のArticleレコードをそれぞれ取得してみます。

docker compose exec app rails c
Loading development environment (Rails 7.0.8.6)

irb(main):001> Article.find(1)
  Article Load (1.0ms)  SELECT `articles`.* FROM `articles` WHERE `articles`.`id` = 1 LIMIT 1 /*application:TestApp*/
An error occurred when inspecting the object: #<ActiveRecord::Encryption::Errors::Decryption: ActiveRecord::Encryption::Errors::Decryption>

irb(main):002> Article.find(2)
  Article Load (1.5ms)  SELECT `articles`.* FROM `articles` WHERE `articles`.`id` = 2 LIMIT 1 /*application:TestApp*/
=>
#<Article:0x0000ffff658be880
 id: 2,
 title: "title",
 body: "body",
 created_at: Sun, 22 Dec 2024 20:50:58.009958000 JST +09:00,
 updated_at: Sun, 22 Dec 2024 20:50:58.009958000 JST +09:00>

このように、暗号化されているデータを取得することはできますが、非暗号化データを取得しようとすると、Active Recordが非暗号化データを復号しようとするため、エラーが発生します。
そのため、Active Recordには、非暗号化データを取得してもエラーが発生しないようにするためのオプションが用意されています。

config/application.rb
module TestApp
  class Application < Rails::Application
+   config.active_record.encryption.support_unencrypted_data = true
  end
end

このように記述することで、非暗号化データを取得してもエラーが発生しないようになります。

では、再度Articleレコードを取得してみます。

docker compose exec app rails c
Loading development environment (Rails 7.0.8.6)

irb(main):001> Article.find(1)
  Article Load (0.4ms)  SELECT `articles`.* FROM `articles` WHERE `articles`.`id` = 1 LIMIT 1 /*application:TestApp*/
=>
#<Article:0x0000ffff9315ca28
 id: 1,
 title: "title",
 body: "body",
 created_at: Sun, 22 Dec 2024 20:47:02.622943000 JST +09:00,
 updated_at: Sun, 22 Dec 2024 20:47:02.622943000 JST +09:00>

irb(main):002> Article.find(2)
  Article Load (1.2ms)  SELECT `articles`.* FROM `articles` WHERE `articles`.`id` = 2 LIMIT 1 /*application:TestApp*/
=>
#<Article:0x0000ffff93885980
 id: 2,
 title: "title",
 body: "body",
 created_at: Sun, 22 Dec 2024 20:50:58.009958000 JST +09:00,
 updated_at: Sun, 22 Dec 2024 20:50:58.009958000 JST +09:00>

無事、Articleレコードを取得することができました。

非暗号化データを暗号化する

現状、データベースには、以下のように非暗号化データと暗号化済みデータが混在しています。

docker compose exec db mysql -u root -p

mysql> USE test_app_development;
Database changed

mysql> SELECT * FROM articles\G;
*************************** 1. row ***************************
        id: 1
     title: title
      body: body
created_at: 2024-12-22 11:47:02.622943
updated_at: 2024-12-22 11:47:02.622943
*************************** 2. row ***************************
        id: 2
     title: title
      body: {"p":"a1rcfg==","h":{"iv":"CybdD1MAo/2fxlYl","at":"UCbsKHRL8/8jmrw6snAYSQ=="}}
created_at: 2024-12-22 11:50:58.009958
updated_at: 2024-12-22 11:50:58.009958
2 rows in set (0.00 sec)

非暗号化データを暗号化する場合は、以下のようにActive Recordを使用して暗号化することができます。

docker compose exec app rails c
Loading development environment (Rails 7.0.8.6)

irb(main):001> Article.all.map(&:encrypt)
  Article Load (2.7ms)  SELECT `articles`.* FROM `articles` /*application:TestApp*/
  TRANSACTION (0.1ms)  BEGIN /*application:TestApp*/
  Article Update (1.1ms)  UPDATE `articles` SET `articles`.`body` = '{\"p\":\"CNXG0g==\",\"h\":{\"iv\":\"ConRjMsP+GNfsD4F\",\"at\":\"XucI7X82OnjrfsrMoH5cnw==\"}}' WHERE `articles`.`id` = 1 /*application:TestApp*/
  TRANSACTION (4.2ms)  COMMIT /*application:TestApp*/
  TRANSACTION (0.1ms)  BEGIN /*application:TestApp*/
  Article Update (0.4ms)  UPDATE `articles` SET `articles`.`body` = '{\"p\":\"TFR5og==\",\"h\":{\"iv\":\"gSszdP/4VDBF7XFW\",\"at\":\"gQ2Cgw337rtLWDlTqs9xIA==\"}}' WHERE `articles`.`id` = 2 /*application:TestApp*/
  TRANSACTION (1.3ms)  COMMIT /*application:TestApp*/
=> [nil, nil]

これで、既存の非暗号化データが暗号化されました。

実際にデータベースでArticleレコードを確認してみます。

docker compose exec db mysql -u root -p

mysql> USE test_app_development;
Database changed

mysql> SELECT * FROM articles\G;
*************************** 1. row ***************************
        id: 1
     title: title
      body: {"p":"CNXG0g==","h":{"iv":"ConRjMsP+GNfsD4F","at":"XucI7X82OnjrfsrMoH5cnw=="}}
created_at: 2024-12-22 11:47:02.622943
updated_at: 2024-12-22 11:47:02.622943
*************************** 2. row ***************************
        id: 2
     title: title
      body: {"p":"TFR5og==","h":{"iv":"gSszdP/4VDBF7XFW","at":"gQ2Cgw337rtLWDlTqs9xIA=="}}
created_at: 2024-12-22 11:50:58.009958
updated_at: 2024-12-22 11:50:58.009958
2 rows in set (0.00 sec)

データベースに保存されているbodyカラムが暗号化されていることが確認できました。

まとめ

今回、初めてActive Record暗号化を試してみました。
意外と簡単に設定することができました。

今回は、Articleテーブルのbodyカラムを暗号化してみましたが、実務では、メールアドレスや住所などの個人情報を暗号化することが多いかと思います。
万が一、データベースに不正アクセスされた場合の保険として、Active Record暗号化を使用しても良いかもしれません。

最後まで読んでいただき、ありがとうございました!

参考文献

https://railsguides.jp/active_record_encryption.html

https://zenn.dev/yagince/articles/2da8cc83d09136

https://www.gmo.jp/security/ciphersecurity/encryption/

GitHubで編集を提案
株式会社L&E Group

Discussion