mcpField
A
active-record-encryption

active-record-encryption

github.com/thinkoodle安全7171 次安装7 周前更新
代码仓库

关于 active-record-encryption

面向 Rails 开发者的 Active Record Encryption 实践技能,指导应用层加密敏感属性。涵盖密钥配置、查询取舍、数据迁移与测试中的常见问题。

主要功能

  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 加密专家

使用 Rails 内置的 Active Record Encryption 在应用层加密敏感数据。

理念

核心原则:

  1. 只加密必要的数据 — 加密会增加复杂性和存储开销。请谨慎决定。
  2. 只有需要查询时才使用确定性加密 — 非确定性加密更安全。默认使用非确定性加密。
  3. 密钥应存放在凭据中,而不是代码里 — 绝不要提交加密密钥。
  4. 加密前先规划迁移 — 现有未加密数据需要迁移策略。
  5. 启用加密后进行测试 — Fixtures 需要设置 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 内容
  • 调试加密相关错误(解密失败、查询不匹配)
  • 了解确定性与非确定性加密之间的权衡

需要避免的关键错误

🚨 这些是 Agent 最常犯的错误。编写代码前请先阅读。

  1. 查询非确定性字段 — Model.find_by(field: value) 只有在设置 deterministic: true 时才有效。非确定性加密每次都会生成不同的密文,因此数据库无法匹配。

  2. 忘记生成或配置密钥 — 未运行 rails db:encryption:init 并将密钥存入凭据时,encrypts :email 不会发挥任何作用。你会遇到 ActiveRecord::Encryption::Errors::Configuration 错误。

  3. 未使用确定性模式就加密需要索引的字段 — 对非确定性列创建的数据库索引没有用。如果需要唯一索引,请使用 deterministic: true。

  4. 未规划现有数据的处理方式 — 对已有记录的模型添加 encrypts 会导致读取失败,因为 Rails 会尝试解密明文值。迁移期间请启用 support_unencrypted_data。

  5. 使用 ignore_case: true 却未添加对应列 — 此选项要求数据库中存在 original_<column_name> 列。缺少该列会导致崩溃。

  6. 在 encrypts 之后声明 serialize — 对于序列化属性,模型中的 serialize 必须写在 encrypts 前面。Rails 会按顺序处理这些声明,而加密需要封装已序列化的值。

  7. 尝试为确定性加密轮换密钥 — 确定性加密不支持密钥轮换,因为相同的明文必须始终生成相同的密文(查询依赖这一点)。更改密钥会导致所有查询静默失效。

  8. 字符串列长度不足 — 加密后的数据载荷更大。加密后,string(255) 类型的电子邮件列至少需要使用 string(510)。列长度指南请参阅 reference.md。

操作说明

步骤 1:生成并存储密钥

从这里开始。 如果未配置密钥,encrypts 声明会在运行时静默产生配置错误。

bin/rails db:encryption:init

这会输出三个值。请将它们存入凭据:

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})\" }"

遵循项目现有约定 — 如果项目使用环境变量,就使用环境变量;如果使用凭据,就使用凭据。

步骤 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)

如果需要通过非确定性字段查找,有两个选择:

  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. 加密所有内容——只加密真正敏感的字段(个人身份信息、财务数据、健康信息)。加密 name 或 created_at 只会增加开销,不会提升安全性。
  2. 在非确定性加密足够时使用确定性加密——确定性加密的安全性较低。只有在需要按该字段查询时才使用。
  3. 忘记迁移计划——对已有数据的模型添加 encrypts,却没有设置 support_unencrypted_data,会导致读取崩溃。
  4. 将密钥存放在代码仓库中——不要放在配置文件、种子文件或代码中的 ENV 默认值里。只能使用凭据或真实的环境变量。
  5. 跳过调整列大小——加密后的数据会更大。列尺寸不足会导致截断,进而造成数据丢失。
  6. 测试时未启用 encrypt_fixtures——Fixture 会加载原始 YAML 值。测试中未启用加密会导致比较失败。
  7. 轮换确定性密钥——这会悄无声息地破坏所有查询。确定性字段必须保留原始密钥。
  8. 在验证中使用 case_sensitive: false——这对加密字段无效。请在 encrypts 上使用 downcase: true 或 ignore_case: true。

评论

支持 Markdown