mcpField
A
active-record-encryption

active-record-encryption

github.com/thinkoodleセキュリティ7171 回インストール7 週間前に更新
リポジトリ

active-record-encryption について

Active Record Encryptionで機密属性をアプリケーション層で暗号化するための、Rails開発者向けスキルです。鍵の設定、検索時の選択、データ移行、テスト時の注意点を扱います。

主な機能

  1. 01鍵と暗号化属性の設定
  2. 02決定的・非決定的暗号化の選択
  3. 03既存の平文データの移行
  4. 04カラム容量と大文字小文字の処理
  5. 05鍵ローテーションとテストの設定

活用シーン

  1. 01モデル属性の暗号化
  2. 02未暗号化レコードの移行
  3. 03決定的暗号化フィールドの検索
  4. 04復号や検索エラーの調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/thinkoodle/rails-skills --skill active-record-encryption

インストール後、Claude Code で/active-record-encryptionを入力して読み込まれたか確認します。

同梱ファイルreference.md

詳細

Active Record Encryption エキスパート

Rails 組み込みの Active Record Encryption を使用して、アプリケーションレベルで機密データを暗号化します。

基本方針

基本原則:

  1. 必要なものだけを暗号化する — 暗号化により、複雑さとストレージのオーバーヘッドが増します。慎重に判断してください。
  2. クエリが必要な場合に限り決定論的暗号化を使う — 非決定論的暗号化のほうが安全です。デフォルトでは非決定論的暗号化を使用してください。
  3. キーはコードではなく credentials に保存する — 暗号化キーをコミットしてはいけません。絶対にです。
  4. 暗号化する前に移行を計画する — 既存の未暗号化データには、移行戦略が必要です。
  5. 暗号化を有効にしてテストする — encrypt_fixtures: true が必要です。設定しないとテストが失敗します。

判断基準:

Need to query/find_by this field?
  YES → deterministic: true
  NO  → default (non-deterministic)

Need uniqueness validation?
  YES → deterministic: true (+ downcase/ignore_case if case-insensitive)

Need to preserve case but query case-insensitively?
  YES → deterministic: true, ignore_case: true (adds original_<column> column)

Just protecting data at rest (logs, backups)?
  YES → default (non-deterministic) is perfect

このスキルを使う場面

  • モデル属性に encrypts 宣言を追加する場合
  • 初めて暗号化キーを設定する場合
  • 既存の未暗号化データを暗号化するために移行する場合
  • 暗号化キーをローテーションする場合
  • 決定論的に暗号化された属性をクエリする場合
  • Action Text のコンテンツを暗号化する場合
  • 暗号化関連のエラー(復号失敗、クエリの不一致)をデバッグする場合
  • 決定論的暗号化と非決定論的暗号化のトレードオフを理解する場合

避けるべき重大な間違い

🚨 エージェントが最もよく犯す間違いです。コードを書く前にお読みください。

  1. 非決定論的フィールドをクエリする — Model.find_by(field: value) が機能するのは、deterministic: true の場合のみです。非決定論的暗号化では、暗号文が毎回異なるため、データベースで一致させられません。

  2. キーの生成や設定を忘れる — rails db:encryption:init を実行してキーを credentials に保存しないと、encrypts :email は何の役にも立ちません。ActiveRecord::Encryption::Errors::Configuration エラーが発生します。

  3. 決定論的モードを使わず、インデックスが必要なフィールドを暗号化する — 非決定論的カラムのデータベースインデックスは役に立ちません。一意インデックスが必要な場合は、deterministic: true を使用してください。

  4. 既存データへの対応を計画しない — 既存の行があるモデルに encrypts を追加すると、Rails が平文の値を復号しようとするため、読み取りに失敗します。移行中は support_unencrypted_data を有効にしてください。

  5. カラムを追加せずに ignore_case: true を使用する — このオプションには、データベース内の original_<column_name> カラムが必要です。これがないとクラッシュします。

  6. serialize を encrypts の後に宣言する — シリアライズされた属性では、モデル内で serialize を encrypts より前に記述する必要があります。Rails はこれらの宣言を順番に処理するため、暗号化でシリアライズ済みの値をラップする必要があります。

  7. 決定論的暗号化のキーをローテーションしようとする — 同じ平文からは常に同じ暗号文が生成される必要があるため(これによってクエリが機能します)、決定論的暗号化ではキーのローテーションはサポートされていません。キーを変更すると、すべての検索がひそかに機能しなくなります。

  8. 文字列カラムのサイズを小さくしすぎる — 暗号化されたデータはサイズが大きくなります。string(255) のメールカラムを暗号化する場合、少なくとも string(510) が必要です。サイズのガイドについては reference.md を参照してください。

手順

ステップ 1:キーを生成して保存する

まずここから始めてください。 キーを設定しないと、encrypts 宣言によって実行時に設定エラーが発生します。

bin/rails db:encryption:init

3 つの値が出力されます。これらを credentials に保存します。

bin/rails credentials:edit
active_record_encryption:
  primary_key: YehXdfzxVKpoLvKseJMJIEGs2JxerkB8
  deterministic_key: uhtk2DYS80OweAPnMLtrV2FhYIXaceAy
  key_derivation_salt: g7Q66StqUQDQk9SJ81sWbYZXgiRogBwS

代替方法:環境変数(12-factor アプリ):

# config/application.rb
config.active_record.encryption.primary_key = ENV["ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY"]
config.active_record.encryption.deterministic_key = ENV["ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY"]
config.active_record.encryption.key_derivation_salt = ENV["ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT"]

キーが設定されていることを確認する:

bin/rails runner "puts ActiveRecord::Encryption.config.primary_key.present?"
# Should print: true
ステップ 2:既存のパターンを確認する

まずコードベースを検索し、既存の暗号化パターンを確認します。

# Find existing encryption declarations
rg "encrypts :" app/models/

# Check for existing encryption config
rg "active_record.encryption" config/
rg "active_record_encryption" config/credentials/

# Check column sizes for fields you plan to encrypt
bin/rails runner "ActiveRecord::Base.connection.columns('users').each { |c| puts \"#{c.name}: #{c.type}(#{c.limit})\" }"

既存のプロジェクト規約に合わせてください — 環境変数を使用している場合は環境変数を使います。credentials を使用している場合は credentials を使います。

ステップ 3:暗号化する属性を宣言する

非決定論的(デフォルト — 最も安全):

class User < ApplicationRecord
  encrypts :ssn
  encrypts :medical_notes
end

決定論的(クエリ可能):

class User < ApplicationRecord
  encrypts :email, deterministic: true
  encrypts :phone, deterministic: true, downcase: true
end

Action Text:

class Message < ApplicationRecord
  has_rich_text :content, encrypted: true
end

シリアライズされた属性を使う場合(順序が重要です!):

class Article < ApplicationRecord
  serialize :metadata, type: Hash    # FIRST
  encrypts :metadata                  # SECOND
end
ステップ 4:カラムサイズを調整する

暗号化する前に、必要に応じてカラムを確認し、サイズを変更してください。

元のカラム暗号化に必要なサイズ
string(255) ASCIIstring(510)
string(255) Unicodestring(1275)
string(500) 非西洋文字string(2255)
texttext(変更不要)

カラムサイズが小さすぎる場合は、マイグレーションを生成してください。

class ResizeEmailForEncryption < ActiveRecord::Migration[7.1]
  def change
    change_column :users, :email, :string, limit: 510
  end
end
ステップ 5:既存の未暗号化データを移行する

テーブルにすでにデータがある場合は、移行を計画してください。 そうしないと、Rails は平文の行を復号しようとして Decryption エラーを発生させます。

フェーズ 1:共存を有効にする

# config/application.rb
config.active_record.encryption.support_unencrypted_data = true
config.active_record.encryption.extend_queries = true

フェーズ 2:既存のレコードを暗号化する(タスクまたはマイグレーションとして実行)

# lib/tasks/encryption.rake
namespace :encryption do
  desc "Encrypt existing user data"
  task encrypt_users: :environment do
    User.find_each do |user|
      user.encrypt
    rescue => e
      Rails.logger.error "Failed to encrypt user #{user.id}: #{e.message}"
    end
  end
end
bin/rails encryption:encrypt_users

フェーズ 3:確認して共存を無効にする

# Verify all records are encrypted
User.find_each do |user|
  unless user.encrypted_attribute?(:email)
    puts "User #{user.id} still unencrypted"
  end
end

すべてのデータが暗号化されたら、共存設定を削除してください。

# Remove these lines:
# config.active_record.encryption.support_unencrypted_data = true
# config.active_record.encryption.extend_queries = true
ステップ 6:キーのローテーションを設定する(非決定的暗号化のみ)

キーのローテーションは非決定的暗号化でのみ機能します。 決定的フィールドは元のキーを維持する必要があります。キーを変更すると暗号文が一致しなくなり、すべてのクエリが機能しなくなります。

新しいキーをリストに追加します。最後のキーで暗号化し、すべてのキーで復号します。

# credentials
active_record_encryption:
  primary_key:
    - old_key_abc123           # Can still decrypt
    - new_key_xyz789           # Active — encrypts new data
  key_derivation_salt: g7Q66StqUQDQk9SJ81sWbYZXgiRogBwS

新しいキーで既存のデータを再暗号化します。

User.find_each(&:encrypt)

再暗号化後、古いキーを削除します。

ステップ 7:テスト環境を設定する

config/environments/test.rb に追加します。

Rails.application.configure do
  config.active_record.encryption.encrypt_fixtures = true
end

これがないと、フィクスチャの値が暗号化されず、読み取りに失敗します。

データを移行する場合は、テスト環境でも共存を有効にします。

config.active_record.encryption.support_unencrypted_data = true
config.active_record.encryption.extend_queries = true
ステップ 8:クエリを適切に処理する

決定的暗号化 — クエリは通常どおり機能します。

# These work with deterministic: true
User.find_by(email: "[email protected]")
User.where(email: "[email protected]")

非決定的暗号化 — クエリは実行できません。

# These can't work with non-deterministic encryption — each encryption
# produces a different ciphertext, so the DB can't match against it
User.find_by(ssn: "123-45-6789")  # => nil (always)
User.where(ssn: "123-45-6789")    # => empty (always)

非決定的フィールドで検索する必要がある場合は、次の 2 つの方法があります。

  1. deterministic: true に切り替える(セキュリティは低下します)
  2. 検索用に別の決定的なダイジェスト/ハッシュカラムを追加する
ステップ 9:一意性と大文字・小文字の区別

一意性のバリデーションには決定的暗号化が必要です。

class User < ApplicationRecord
  validates :email, uniqueness: true
  encrypts :email, deterministic: true, downcase: true
end

バリデーションで case_sensitive: false を使用しないでください — データベースには平文ではなく暗号文が見えるため、暗号化されたフィールドでは機能しません。代わりに encrypts の downcase: true または ignore_case: true を使用してください。暗号化前に大文字・小文字を正規化できます。

downcase: true — 元の大文字・小文字は失われます。シンプルです。

ignore_case: true — 元の大文字・小文字を保持しますが、追加のカラムが必要です。

# Migration needed:
add_column :labels, :original_name, :string

# Model:
class Label < ApplicationRecord
  encrypts :name, deterministic: true, ignore_case: true
end

クイックリファレンス

encrypts のオプション
オプションデフォルト目的
deterministic:falseクエリ可能な暗号化を有効にする
downcase:false暗号化前に小文字にする(決定的暗号化のみ)
ignore_case:false大文字・小文字を区別しないクエリを可能にし、元の値を保持する(original_ カラムが必要)
key_provider:globalこの属性用のカスタムキー・プロバイダー
key:nilこの属性用の特定の暗号化キー
previous:nil移行用の以前の暗号化方式
compress:trueペイロードの圧縮を有効にする
compressor:Zlibカスタム圧縮アルゴリズム
message_serializer:default暗号化ペイロード用のカスタムシリアライザー
設定オプション
# config/application.rb
config.active_record.encryption.primary_key = "..."
config.active_record.encryption.deterministic_key = "..."
config.active_record.encryption.key_derivation_salt = "..."
config.active_record.encryption.support_unencrypted_data = false  # true during migration
config.active_record.encryption.extend_queries = false             # true during migration
config.active_record.encryption.encrypt_fixtures = false           # true in test env
config.active_record.encryption.add_to_filter_parameters = true    # auto-filter in logs
config.active_record.encryption.store_key_references = false       # faster decryption, larger payload
config.active_record.encryption.forced_encoding_for_deterministic_encryption = Encoding::UTF_8
プログラムから使用する API
record.encrypt                          # Encrypt/re-encrypt all encryptable attributes
record.decrypt                          # Decrypt all encryptable attributes
record.encrypted_attribute?(:field)     # Check if attribute is currently encrypted
record.ciphertext_for(:field)           # Read raw ciphertext

# Run code without encryption (reads return ciphertext, writes store plaintext)
ActiveRecord::Encryption.without_encryption { ... }

# Run code that can read encrypted data but can't overwrite it
ActiveRecord::Encryption.protecting_encrypted_data { ... }
よくあるエラー → 解決方法
エラー原因解決方法
ActiveRecord::Encryption::Errors::Configurationキーが設定されていないrails db:encryption:init を実行し、認証情報に保存する
ActiveRecord::Encryption::Errors::Decryptionキーが間違っている、またはデータが破損しているキー設定を確認する。移行中は support_unencrypted_data を有効にする
既存データのクエリ結果が nil非決定的フィールドをクエリしているencrypts の宣言に deterministic: true を追加する
undefined column 'original_X'マイグレーションを行わずに ignore_case: true を使用しているマイグレーションで original_<column> カラムを追加する
フィクスチャのテストが失敗するフィクスチャが暗号化されていないテスト設定で encrypt_fixtures = true を設定する
シリアライズされた属性が気付かないうちに壊れている宣言の順序が間違っているserialize を encrypts より前に記述する

デバッグのヒント

# Check if encryption is properly configured
bin/rails runner "pp ActiveRecord::Encryption.config"

# Check if a specific record's attribute is encrypted
bin/rails runner "puts User.first.encrypted_attribute?(:email)"

# Read raw ciphertext
bin/rails runner "puts User.first.ciphertext_for(:email)"

# Temporarily disable encryption to see raw database values
ActiveRecord::Encryption.without_encryption do
  puts User.first.read_attribute(:email)
end

# Check what's actually stored in the database
bin/rails runner "puts ActiveRecord::Base.connection.select_value(\"SELECT email FROM users LIMIT 1\")"

避けるべきアンチパターン

  1. すべてを暗号化する — 本当に機密性の高いフィールド(PII、金融データ、健康情報)のみを暗号化します。name や created_at を暗号化しても、セキュリティ上のメリットはなく、オーバーヘッドが増えるだけです。
  2. 非決定的な暗号化で十分なのに、決定的な暗号化を使う — 決定的な暗号化は安全性が低くなります。そのフィールドで検索する必要がある場合にのみ使用してください。
  3. 移行計画を忘れる — 既存のデータがあるモデルに support_unencrypted_data なしで encrypts を追加すると、読み取り時にエラーが発生します。
  4. キーをリポジトリに保存する — 設定ファイルにも、シードにも、コード内の ENV のデフォルト値にも保存しないでください。認証情報または実際の環境変数のみを使用してください。
  5. カラムのサイズ変更を省略する — 暗号化されたデータはサイズが大きくなります。カラムのサイズが不足すると、切り詰められてデータが失われます。
  6. encrypt_fixtures なしでテストする — フィクスチャは未加工の YAML 値を読み込みます。テスト時に暗号化しないと、比較が失敗します。
  7. 決定的な暗号化のキーをローテーションする — すべてのクエリが警告なしに機能しなくなります。決定的に暗号化するフィールドでは、元のキーを維持する必要があります。
  8. バリデーションで case_sensitive: false を使う — 暗号化されたフィールドでは機能しません。encrypts に downcase: true または ignore_case: true を使用してください。

コメント