关于 active-record-encryption
面向 Rails 开发者的 Active Record Encryption 实践技能,指导应用层加密敏感属性。涵盖密钥配置、查询取舍、数据迁移与测试中的常见问题。
主要功能
- 01配置加密密钥与属性
- 02选择确定性或非确定性加密
- 03迁移现有明文数据
- 04处理列容量与大小写
- 05配置密钥轮换和测试
适用场景
- 01为模型属性添加加密
- 02迁移已有未加密记录
- 03查询确定性加密字段
- 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 在应用层加密敏感数据。
理念
核心原则:
- 只加密必要的数据 — 加密会增加复杂性和存储开销。请谨慎决定。
- 只有需要查询时才使用确定性加密 — 非确定性加密更安全。默认使用非确定性加密。
- 密钥应存放在凭据中,而不是代码里 — 绝不要提交加密密钥。
- 加密前先规划迁移 — 现有未加密数据需要迁移策略。
- 启用加密后进行测试 — 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 最常犯的错误。编写代码前请先阅读。
-
查询非确定性字段 —
Model.find_by(field: value)只有在设置deterministic: true时才有效。非确定性加密每次都会生成不同的密文,因此数据库无法匹配。 -
忘记生成或配置密钥 — 未运行
rails db:encryption:init并将密钥存入凭据时,encrypts :email不会发挥任何作用。你会遇到ActiveRecord::Encryption::Errors::Configuration错误。 -
未使用确定性模式就加密需要索引的字段 — 对非确定性列创建的数据库索引没有用。如果需要唯一索引,请使用
deterministic: true。 -
未规划现有数据的处理方式 — 对已有记录的模型添加
encrypts会导致读取失败,因为 Rails 会尝试解密明文值。迁移期间请启用support_unencrypted_data。 -
使用
ignore_case: true却未添加对应列 — 此选项要求数据库中存在original_<column_name>列。缺少该列会导致崩溃。 -
在
encrypts之后声明serialize— 对于序列化属性,模型中的serialize必须写在encrypts前面。Rails 会按顺序处理这些声明,而加密需要封装已序列化的值。 -
尝试为确定性加密轮换密钥 — 确定性加密不支持密钥轮换,因为相同的明文必须始终生成相同的密文(查询依赖这一点)。更改密钥会导致所有查询静默失效。
-
字符串列长度不足 — 加密后的数据载荷更大。加密后,
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) ASCII | string(510) |
string(255) Unicode | string(1275) |
string(500) 非西方字符 | string(2255) |
text | text(无需更改) |
如果列太小,请生成迁移:
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)
如果需要通过非确定性字段查找,有两个选择:
- 改用
deterministic: true(安全性较低) - 添加单独的确定性摘要/哈希列用于查找
步骤 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\")"
应避免的反模式
- 加密所有内容——只加密真正敏感的字段(个人身份信息、财务数据、健康信息)。加密
name或created_at只会增加开销,不会提升安全性。 - 在非确定性加密足够时使用确定性加密——确定性加密的安全性较低。只有在需要按该字段查询时才使用。
- 忘记迁移计划——对已有数据的模型添加
encrypts,却没有设置support_unencrypted_data,会导致读取崩溃。 - 将密钥存放在代码仓库中——不要放在配置文件、种子文件或代码中的 ENV 默认值里。只能使用凭据或真实的环境变量。
- 跳过调整列大小——加密后的数据会更大。列尺寸不足会导致截断,进而造成数据丢失。
- 测试时未启用
encrypt_fixtures——Fixture 会加载原始 YAML 值。测试中未启用加密会导致比较失败。 - 轮换确定性密钥——这会悄无声息地破坏所有查询。确定性字段必须保留原始密钥。
- 在验证中使用
case_sensitive: false——这对加密字段无效。请在encrypts上使用downcase: true或ignore_case: true。