Railsで暗号化されたデータをクエリする:決定論的暗号化の活用
現代のRailsアプリケーションにおいて、データ暗号化はもはや必須要件となっています。特にメールアドレス、電話番号、マイナンバー、マイナンバーカード、銀行口座情報など、機密性の高い個人情報の保護が急務です。しかし、データを暗号化した直後に新たな問題が生じます:暗号化されたカラムに対して、効率的にクエリ(検索)を実行するにはどうすればよいか?
Rails 7からは、Active Record Encryptionという機能が導入され、その中に**決定論的暗号化(Deterministic Encryption)**という仕組みが含まれました。これにより、データを暗号化しながらも、暗号化されたフィールドに対して検索機能を実装できるようになり、セキュリティと利便性のバランスが格段に改善されました。
決定論的暗号化とは?
決定論的暗号化とは、同じプレーンテキスト(元のデータ)を同じキーで暗号化すると、常に同じ暗号文(暗号化されたデータ)が生成される暗号化手法です。
これは「ランダム暗号化」(非決定論的暗号化)とは対照的です。ランダム暗号化では、同じ値を暗号化するたびに異なる暗号文が生成されるため、セキュリティは高まりますが、その値を用いてのクエリはほぼ不可能になります。
なぜ決定論的暗号化がクエリに適しているのか?
同じ入力値が常に同じ暗号文を生成するため、Railsは自動的にクエリ内の値を暗号化してから、データベースに保存されている暗号文と比較することができます。
したがって、以下のようにシンプルに記述できます:
User.find_by(email: "user@example.com")
Railsが自動的に"user@example.com"を決定論的キーで暗号化し、その後データベースに照会します。
典型的な使用例
機密性の高いデータで、暗号化と検索の両立が必要なケースは多くあります:
- メールアドレス:ユーザーをメールで検索したいが、DB内には暗号化された状態で保存したい。
- 識別番号(マイナンバー、マイナンバーカード番号、顧客IDなど):識別番号で記録を検索したいが、プレーンテキストは保存しない。
- 電話番号:顧客ルックアップに使用するが、DB侵害時の被害を最小化する必要がある。
ランダム暗号化を使用する場合、同じ電話番号は毎回異なる暗号文を生成するため、where(phone: "...")のようなクエリは実質的に不可能です。決定論的暗号化がこの課題を解決します。
Railsでの決定論的暗号化の設定
Railsで決定論的暗号化を利用するには、アプリケーションレベルでActive Record Encryptionを構成する必要があります。
1. configでキーを宣言する
config/application.rbに以下の設定を追加します:
# config/application.rb
config.active_record.encryption.primary_key = Rails.application.credentials
.active_record_encryption[:primary_key]
config.active_record.encryption.deterministic_key = Rails.application.credentials
.active_record_encryption[:deterministic_key]
config.active_record.encryption.key_derivation_salt = Rails.application.credentials
.active_record_encryption[:key_derivation_salt]
これらの値はRails.credentialsまたは安全なシークレット管理ツールで管理する必要があり、コードベースにハードコードしてはいけません。
2. Rakeタスクでキーを初期化する
Railsが用意しているRakeタスクを使用してキーを生成できます:
rails db:encryption:init
このコマンドはキーを生成し、対応するprimary_key、deterministic_key、key_derivation_saltをcredentialsに保存するよう促します。
モデルに決定論的暗号化を追加する
設定後、次のステップはモデルで暗号化対象の属性をencryptsマクロで宣言することです。
Userモデルの例:
class User < ApplicationRecord
encrypts :email, deterministic: true
encrypts :my_number, deterministic: true
end
-
deterministic: trueは、このフィールドに対するクエリを有効にします。 - Railsは読み書き時に自動的に暗号化・復号化を行います。
データベースを確認するとemailやmy_numberは暗号文で保存されていますが、アプリケーション側ではプレーンテキストとして扱えます。
暗号化されたカラムに対するクエリ
決定論的暗号化の最大の利点は、クエリの書き方を変更する必要がないことです。
例:
# メールアドレスでユーザーを検索
user = User.find_by(email: "user@example.com")
# マイナンバーで検索
user = User.where(my_number: "123-45-6789").first
どちらのケースでも:
- Railsは
"user@example.com"または"123-45-6789"を決定論的キーで暗号化します。 - 暗号化された値をDBの値と比較します。
おなじみのActive Record APIを維持しながら、データベース内のデータは常に暗号化された状態で保護されます。
決定論的暗号化とランダム暗号化の比較
決定論的暗号化の特性
- 同じプレーンテキスト → 同じキーを使用すれば常に同じ暗号文。
- インデックスおよびクエリ(WHERE句、UNIQUEインデックスなど)がカラムに対して使用可能。
- セキュリティは若干低下します。パターン分析により、複数の同一暗号文レコード = 重複するプレーンテキスト の可能性が推測されるため。
- 検索が必要なフィールドに適しています:メール、電話番号、顧客コード、識別番号。
ランダム暗号化の特性
- 同じプレーンテキスト → 暗号化するたびに異なる暗号文。
- 元の値でのダイレクトなクエリは不可能。暗号文ベースのインデックスも使用できない。
- パターン分析に対する防御が優れています。攻撃者は同一値を特定できません。
- 読み取り・表示のみが必要なデータに適しています:機密性の高い財務情報、監査ログなど。
単一モデル内での両者の組み合わせ
class User < ApplicationRecord
# メールアドレスは検索可能にする
encrypts :email, deterministic: true
# クレジットカード番号は表示時のみ復号化、クエリ不可
encrypts :credit_card_number
end
emailは決定論的暗号化で検索機能を実現し、credit_card_numberはランダム暗号化で強化されたセキュリティを提供します。
決定論的暗号化を使用すべき場合(と使用すべきでない場合)
決定論的暗号化を使用すべき場合:
- フィールドがデータベース内で検索またはインデックスされる必要がある。
- パターン露出(複数の同一暗号文レコード)がセキュリティリスクの大きさを超えない。
- ビジネス要件が機密データに対する高速ルックアップを要求している(メール/電話番号検索、顧客照会など)。
決定論的暗号化を使用すべきでない場合:
- パスワードなど機密性が極めて高いデータ(必ずbcrypt/argon2などのハッシング関数を使用し、暗号化は避けるべき)。
- 保存と表示のみが必要で、クエリ要件がない。
- パターン露出(値の重複)がセキュリティの重大な脅威となる場合。
スキーマ設計時には、各フィールドを分類することが重要です:どのフィールドが検索必須か、どのフィールドが読み出しのみか。その分類に基づいて、決定論的または非決定論的の暗号化を選択します。
結論
Rails 7以降のActive Record Encryptionに含まれる決定論的暗号化は、セキュリティとデータ検索能力のバランスを実現する実用的なソリューションです。
encrypts ... deterministic: trueを活用することで、Railsアプリケーションはメールアドレス、電話番号、マイナンバーなどの重要なフィールドを暗号化しながら、おなじみのActive Record構文でクエリを記述できます。
システム設計時に、決定論的暗号化とランダム暗号化の強みと弱みを理解しておくことは、セキュリティ要件を損なわないデータ保護戦略を立案する際に極めて重要です。利便性とセキュリティのどちらも妥協する必要はありません。