Active Record 支持应用层加密。它通过声明哪些属性应该被加密,并在需要时无缝地加密和解密它们。加密层位于数据库和应用程序之间。应用程序将访问未加密的数据,但数据库将存储加密后的数据。
1. 为什么要进行应用层数据加密?
Active Record 加密旨在保护应用程序中的敏感信息。一个典型的例子是用户的个人身份信息。但是,如果您已经对数据库进行了静态加密,为什么还需要应用层加密呢?
作为一个直接的实际好处,加密敏感属性增加了一个额外的安全层。例如,如果攻击者获得了对您的数据库、其快照或您的应用程序日志的访问权限,他们将无法理解加密的信息。此外,加密可以防止开发人员在应用程序日志中无意中暴露用户的敏感数据。
但更重要的是,通过使用 Active Record 加密,您可以在代码级别定义应用程序中的敏感信息。Active Record 加密使您能够对应用程序以及使用应用程序数据的服务中的数据访问进行精细控制。例如,考虑可审计的 Rails 控制台,它们可以保护加密数据,或者查看内置系统以自动过滤控制器参数。
2. 基本用法
2.1. 设置
运行 bin/rails db:encryption:init 生成一个随机密钥集。
$ bin/rails db:encryption:init
Add this entry to the credentials of the target environment:
active_record_encryption:
primary_key: EGY8WhulUOXixybod7ZWwMIL68R9o5kC
deterministic_key: aPA5XyALhf75NNnMzaspW7akTfZp0lPY
key_derivation_salt: xEY0dt6TZcAMg52K7O84wYzkjvbA62Hz
通过复制和粘贴生成的值到您现有的 Rails 凭证中来存储这些值。或者,这些值也可以从其他来源(例如环境变量)配置。
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"]
这些生成的值长度为 32 字节。如果您自行生成,主要密钥(这将用于导出 AES 32 字节密钥)的最小长度应为 12 字节,盐值的最小长度应为 20 字节。
2.2. 加密属性声明
可加密属性在模型级别定义。这些是常规的 Active Record 属性,由具有相同名称的列支持。
class Article < ApplicationRecord
encrypts :title
end
该库将在将这些属性保存到数据库之前透明地加密它们,并在检索时解密它们。
article = Article.create title: "Encrypt it all!"
article.title # => "Encrypt it all!"
但是,在底层,执行的 SQL 看起来像这样:
INSERT INTO `articles` (`title`) VALUES ('{\"p\":\"n7J0/ol+a7DRMeaE\",\"h\":{\"iv\":\"DXZMDWUKfp3bg/Yu\",\"at\":\"X1/YjMHbHD4talgF9dt61A==\"}}')
2.2.1. 重要提示:关于存储和列大小
加密需要额外的空间,因为 Base64 编码和与加密负载一起存储的元数据。当使用内置的信封加密密钥提供程序时,最坏情况下的开销估计约为 255 字节。这种开销在较大尺寸下可以忽略不计。不仅因为开销会被稀释,还因为该库默认使用压缩,对于较大的负载,与未加密版本相比,可以节省高达 30% 的存储空间。
关于字符串列大小有一个重要问题:在现代数据库中,列大小决定了它能分配的“字符数”,而不是字节数。例如,对于 UTF-8,每个字符最多可以占用四个字节,因此,在使用 UTF-8 的数据库中,一列的存储空间在“字节数”方面可能达到其大小的四倍。现在,加密负载是序列化为 Base64 的二进制字符串,因此它们可以存储在常规的 string 列中。由于它们是 ASCII 字节序列,因此加密列的大小可能达到其明文版本的四倍。所以,即使数据库中存储的字节相同,列也必须是四倍大。
实际上,这意味着:
- 当加密使用西方字母(主要是 ASCII 字符)编写的短文本时,在定义列大小时应考虑额外的 255 字节开销。
- 当加密使用非西方字母(如西里尔字母)编写的短文本时,应将列大小乘以 4。请注意,存储开销最多为 255 字节。
- 当加密长文本时,您可以忽略列大小问题。
一些例子:
| 要加密的内容 | 原始列大小 | 推荐的加密列大小 | 存储开销(最坏情况) |
|---|---|---|---|
| 电子邮件地址 | string(255) | string(510) | 255 字节 |
| 短表情符号序列 | string(255) | string(1020) | 255 字节 |
| 非西方字母编写文本的摘要 | string(500) | string(2000) | 255 字节 |
| 任意长文本 | text | text | 可忽略 |
2.3. 确定性加密和非确定性加密
默认情况下,Active Record 加密采用非确定性加密方法。在此上下文中,非确定性意味着使用相同密码两次加密相同内容将产生不同的密文。这种方法通过使密文的密码分析更困难、并且使查询数据库不可能来提高安全性。
您可以使用 deterministic: 选项以确定性方式生成初始化向量,从而有效地启用对加密数据的查询。
class Author < ApplicationRecord
encrypts :email, deterministic: true
end
Author.find_by_email("some@email.com") # You can query the model normally
除非您需要查询数据,否则建议使用非确定性方法。
在非确定性模式下,Active Record 使用带有 256 位密钥和随机初始化向量的 AES-GCM。在确定性模式下,它也使用 AES-GCM,但初始化向量是作为密钥和要加密内容的 HMAC-SHA-256 摘要生成的。
您可以不提供 deterministic_key 来禁用确定性加密。
3. 功能
3.1. Action Text
您可以通过在声明中传递 encrypted: true 来加密 Action Text 属性。
class Message < ApplicationRecord
has_rich_text :content, encrypted: true
end
目前尚不支持为 Action Text 属性传递单独的加密选项。它将使用非确定性加密和全局配置的加密选项。
3.2. Fixtures
您可以通过在 test.rb 中添加此选项来自动加密 Rails fixtures。
config.active_record.encryption.encrypt_fixtures = true
启用后,所有可加密属性将根据模型中定义的加密设置自动加密。
3.2.1. Action Text Fixtures
要加密 Action Text fixtures,您应该将它们放在 fixtures/action_text/encrypted_rich_texts.yml 中。
3.3. 支持的类型
active_record.encryption 将在加密值之前使用底层类型对其进行序列化,但是,除非使用自定义的 message_serializer,否则“它们必须可以序列化为字符串”。像 serialized 这样的结构化类型是开箱即用的。
如果您需要支持自定义类型,推荐的方法是使用序列化属性。序列化属性的声明应位于加密声明“之前”。
# CORRECT
class Article < ApplicationRecord
serialize :title, type: Title
encrypts :title
end
# INCORRECT
class Article < ApplicationRecord
encrypts :title
serialize :title, type: Title
end
3.4. 忽略大小写
在查询确定性加密数据时,您可能需要忽略大小写。有两种方法可以更容易地实现这一点:
您可以在声明加密属性时使用 :downcase 选项,在加密发生之前将内容转换为小写。
class Person
encrypts :email_address, deterministic: true, downcase: true
end
当使用 :downcase 时,原始大小写会丢失。在某些情况下,您可能希望在查询时忽略大小写,同时存储原始大小写。对于这些情况,您可以使用 :ignore_case 选项。这需要您添加一个名为 original_<column_name> 的新列,以存储大小写不变的内容。
class Label
encrypts :name, deterministic: true, ignore_case: true # the content with the original case will be stored in the column `original_name`
end
3.5. 支持未加密数据
为了简化未加密数据的迁移,该库包含了选项 config.active_record.encryption.support_unencrypted_data。当设置为 true 时:
- 尝试读取未加密的加密属性将正常工作,而不会引发任何错误。
- 包含确定性加密属性的查询将包含它们的“明文”版本,以支持查找加密和未加密内容。您需要设置
config.active_record.encryption.extend_queries = true来启用此功能。
此选项旨在在过渡期使用,即明文数据和加密数据必须共存时。两者默认都设置为 false,这是任何应用程序的推荐目标:在处理未加密数据时将引发错误。
3.6. 支持以前的加密方案
更改属性的加密属性可能会破坏现有数据。例如,假设您想将确定性属性变为非确定性。如果您只更改模型中的声明,读取现有密文将失败,因为加密方法现在不同了。
为了支持这些情况,您可以声明以前的加密方案,这些方案将在两种场景中使用:
- 在读取加密数据时,如果当前方案不起作用,Active Record 加密将尝试以前的加密方案。
- 在查询确定性数据时,它将添加使用以前方案的密文,以便查询与使用不同方案加密的数据无缝工作。您必须设置
config.active_record.encryption.extend_queries = true来启用此功能。
您可以配置以前的加密方案:
- 全局
- 按属性
3.6.1. 全局以前的加密方案
您可以通过在 application.rb 中使用 previous 配置属性将它们作为属性列表添加来添加以前的加密方案。
config.active_record.encryption.previous = [ { key_provider: MyOldKeyProvider.new } ]
3.6.2. 按属性加密方案
在声明属性时使用 :previous。
class Article
encrypts :title, deterministic: true, previous: { deterministic: false }
end
3.6.3. 加密方案和确定性属性
添加以前的加密方案时:
- 对于非确定性加密,新信息将始终使用“最新”(当前)加密方案进行加密。
- 对于确定性加密,默认情况下,新信息将始终使用“最旧”的加密方案进行加密。
通常,对于确定性加密,您希望密文保持不变。您可以通过设置 deterministic: { fixed: false } 来更改此行为。在这种情况下,它将使用“最新”的加密方案来加密新数据。
3.7. 唯一约束
唯一约束只能用于确定性加密数据。
3.7.1. 唯一性验证
只要启用了扩展查询(config.active_record.encryption.extend_queries = true),就可以正常支持唯一性验证。
class Person
validates :email_address, uniqueness: true
encrypts :email_address, deterministic: true, downcase: true
end
它们在结合加密和未加密数据时,以及在配置以前的加密方案时也有效。
如果您想忽略大小写,请确保在 encrypts 声明中使用 downcase: 或 ignore_case:。在验证中使用 case_sensitive: 选项将不起作用。
3.7.2. 唯一索引
为了支持确定性加密列上的唯一索引,您需要确保它们的密文永远不会改变。
为了鼓励这一点,当配置了多个加密方案时,确定性属性默认将始终使用可用的最旧加密方案。否则,确保这些属性的加密属性不改变是您的责任,否则唯一索引将不起作用。
class Person
encrypts :email_address, deterministic: true
end
3.8. 过滤名为加密列的参数
默认情况下,加密列被配置为在 Rails 日志中自动过滤。您可以通过在 application.rb 中添加以下内容来禁用此行为:
config.active_record.encryption.add_to_filter_parameters = false
如果过滤已启用,但您希望从自动过滤中排除特定列,请将它们添加到 config.active_record.encryption.excluded_from_filter_parameters 中。
config.active_record.encryption.excluded_from_filter_parameters = [:catchphrase]
生成过滤参数时,Rails 会使用模型名称作为前缀。例如:对于 Person#name,过滤参数将是 person.name。
3.9. 编码
该库将保留非确定性加密字符串值的编码。
由于编码与加密负载一起存储,确定性加密的值默认将强制使用 UTF-8 编码。因此,具有不同编码的相同值在加密时将产生不同的密文。您通常希望避免这种情况,以保持查询和唯一性约束的正常工作,因此该库将自动为您执行转换。
您可以使用以下方式配置确定性加密所需的默认编码:
config.active_record.encryption.forced_encoding_for_deterministic_encryption = Encoding::US_ASCII
您可以通过以下方式禁用此行为并在所有情况下保留编码:
config.active_record.encryption.forced_encoding_for_deterministic_encryption = nil
3.10. 压缩
该库默认压缩加密负载。对于较大的负载,这可以节省高达 30% 的存储空间。您可以通过将加密属性的 compress: 设置为 false 来禁用压缩。
class Article < ApplicationRecord
encrypts :content, compress: false
end
您还可以配置用于压缩的算法。默认的压缩器是 Zlib。您可以通过创建一个响应 #deflate(data) 和 #inflate(data) 的类或模块来实现自己的压缩器。
require "zstd-ruby"
module ZstdCompressor
def self.deflate(data)
Zstd.compress(data)
end
def self.inflate(data)
Zstd.decompress(data)
end
end
class User
encrypts :name, compressor: ZstdCompressor
end
您可以全局配置压缩器:
config.active_record.encryption.compressor = ZstdCompressor
4. 密钥管理
密钥提供程序实现了密钥管理策略。您可以全局或按属性配置密钥提供程序。
4.1. 内置密钥提供程序
4.1.1. DerivedSecretKeyProvider
一个密钥提供程序,它将使用 PBKDF2 从提供的密码派生密钥。
config.active_record.encryption.key_provider = ActiveRecord::Encryption::DerivedSecretKeyProvider.new(["some passwords", "to derive keys from. ", "These should be in", "credentials"])
默认情况下,active_record.encryption 使用 active_record.encryption.primary_key 中定义的密钥配置一个 DerivedSecretKeyProvider。
4.1.2. EnvelopeEncryptionKeyProvider
实现了一个简单的信封加密策略。
- 它为每个数据加密操作生成一个随机密钥。
- 它将数据密钥与数据本身一起存储,使用凭据
active_record.encryption.primary_key中定义的主密钥进行加密。
您可以通过在 application.rb 中添加以下内容来配置 Active Record 使用此密钥提供程序:
config.active_record.encryption.key_provider = ActiveRecord::Encryption::EnvelopeEncryptionKeyProvider.new
与其他内置密钥提供程序一样,您可以在 active_record.encryption.primary_key 中提供主密钥列表,以实现密钥轮换方案。
4.2. 自定义密钥提供程序
对于更高级的密钥管理方案,您可以在初始化程序中配置自定义密钥提供程序:
ActiveRecord::Encryption.key_provider = MyKeyProvider.new
密钥提供程序必须实现以下接口:
class MyKeyProvider
def encryption_key
end
def decryption_keys(encrypted_message)
end
end
这两个方法都返回 ActiveRecord::Encryption::Key 对象。
encryption_key返回用于加密某些内容的密钥。decryption_keys返回用于解密给定消息的潜在密钥列表。
密钥可以包含任意标签,这些标签将与消息一起以未加密的形式存储。您可以使用 ActiveRecord::Encryption::Message#headers 在解密时检查这些值。
4.3. 属性特定密钥提供程序
您可以使用 :key_provider 选项按属性配置密钥提供程序:
class Article < ApplicationRecord
encrypts :summary, key_provider: ArticleKeyProvider.new
end
4.4. 属性特定密钥
您可以使用 :key 选项按属性配置给定密钥:
class Article < ApplicationRecord
encrypts :summary, key: "some secret key for article summaries"
end
Active Record 使用该密钥来派生用于加密和解密数据的密钥。
4.5. 密钥轮换
active_record.encryption 可以使用密钥列表来支持实现密钥轮换方案。
- 最后一个密钥将用于加密新内容。
- 解密内容时将尝试所有密钥,直到其中一个起作用。
active_record_encryption:
primary_key:
- a1cc4d7b9f420e40a337b9e68c5ecec6 # Previous keys can still decrypt existing content
- bc17e7b413fd4720716a7633027f8cc4 # Active, encrypts new content
key_derivation_salt: a3226b97b3b2f8372d1fc6d497a0c0d3
这使得工作流可以通过添加新密钥、重新加密内容和删除旧密钥来维护一个简短的密钥列表。
目前不支持确定性加密的密钥轮换。
Active Record 加密尚未提供密钥轮换过程的自动管理。所有组件都已就位,但尚未实现。
4.6. 存储密钥引用
您可以配置 active_record.encryption.store_key_references,使 active_record.encryption 在加密消息本身中存储对加密密钥的引用。
config.active_record.encryption.store_key_references = true
这样做可以提高解密性能,因为系统现在可以直接定位密钥,而不是尝试密钥列表。代价是存储:加密数据会稍微大一些。
5. API
5.1. 基本 API
ActiveRecord 加密旨在以声明方式使用,但它为高级使用场景提供了 API。
5.1.1. 加密和解密
article.encrypt # encrypt or re-encrypt all the encryptable attributes
article.decrypt # decrypt all the encryptable attributes
5.1.2. 读取密文
article.ciphertext_for(:title)
5.1.3. 检查属性是否已加密
article.encrypted_attribute?(:title)
6. 配置
6.1. 配置选项
您可以在 application.rb(最常见的情况)或特定的环境配置文件 config/environments/<env name>.rb 中配置 Active Record 加密选项,如果您想按环境设置它们。
建议使用 Rails 内置的凭证支持来存储密钥。如果您更喜欢通过配置属性手动设置它们,请确保不要将它们与您的代码一起提交(例如,使用环境变量)。
6.1.1. config.active_record.encryption.support_unencrypted_data
当为 true 时,未加密数据可以正常读取。当为 false 时,将引发错误。默认值:false。
6.1.2. config.active_record.encryption.extend_queries
当为 true 时,引用确定性加密属性的查询将根据需要进行修改以包含附加值。这些附加值将是值的明文版本(当 config.active_record.encryption.support_unencrypted_data 为 true 时)以及使用以前加密方案加密的值(如果有)(如 previous: 选项所提供)。默认值:false(实验性)。
6.1.3. config.active_record.encryption.encrypt_fixtures
当为 true 时,fixture 中的可加密属性在加载时将自动加密。默认值:false。
6.1.4. config.active_record.encryption.store_key_references
当为 true 时,加密密钥的引用存储在加密消息的头部。这使得在使用多个密钥时解密更快。默认值:false。
6.1.5. config.active_record.encryption.add_to_filter_parameters
当为 true 时,加密属性名称会自动添加到config.filter_parameters中,并且不会显示在日志中。默认值:true。
6.1.6. config.active_record.encryption.excluded_from_filter_parameters
当 config.active_record.encryption.add_to_filter_parameters 为 true 时,您可以配置一个参数列表,这些参数不会被过滤掉。默认值:[]。
6.1.7. config.active_record.encryption.validate_column_size
根据列大小添加验证。建议这样做以防止使用高度可压缩的负载存储巨大值。默认值:true。
6.1.8. config.active_record.encryption.primary_key
用于派生根数据加密密钥的密钥或密钥列表。它们的用法取决于配置的密钥提供程序。首选通过 active_record_encryption.primary_key 凭据进行配置。
6.1.9. config.active_record.encryption.deterministic_key
用于确定性加密的密钥或密钥列表。首选通过 active_record_encryption.deterministic_key 凭据进行配置。
6.1.10. config.active_record.encryption.key_derivation_salt
派生密钥时使用的盐。首选通过 active_record_encryption.key_derivation_salt 凭据进行配置。
6.1.11. config.active_record.encryption.forced_encoding_for_deterministic_encryption
确定性加密属性的默认编码。您可以将此选项设置为 nil 来禁用强制编码。默认值为 Encoding::UTF_8。
6.1.12. config.active_record.encryption.hash_digest_class
用于派生密钥的摘要算法。默认值为 OpenSSL::Digest::SHA256。
6.1.13. config.active_record.encryption.support_sha1_for_non_deterministic_encryption
支持使用 SHA1 摘要类解密非确定性加密的数据。默认值为 false,这意味着它将只支持 config.active_record.encryption.hash_digest_class 中配置的摘要算法。
6.1.14. config.active_record.encryption.compressor
用于压缩加密负载的压缩器。它应该响应 deflate 和 inflate。默认值为 Zlib。您可以在压缩部分找到有关压缩器的更多信息。
6.2. 加密上下文
加密上下文定义了在给定时刻使用的加密组件。有一个基于您的全局配置的默认加密上下文,但您可以为给定属性或在运行特定代码块时配置自定义上下文。
加密上下文是一种灵活但高级的配置机制。大多数用户不应该关心它们。
加密上下文的主要组件是:
encryptor:暴露加密和解密数据的内部 API。它与key_provider交互以构建加密消息并处理它们的序列化。加密/解密本身由cipher完成,序列化由message_serializer完成。cipher:加密算法本身(AES 256 GCM)。key_provider:提供加密和解密密钥。message_serializer:序列化和反序列化加密负载(Message)。
如果您决定构建自己的 message_serializer,使用不能反序列化任意对象的安全机制非常重要。一个普遍支持的场景是加密现有未加密数据。攻击者可以利用这一点在加密发生之前输入被篡改的负载并执行 RCE 攻击。这意味着自定义序列化器应避免使用 Marshal、YAML.load(请改用 YAML.safe_load)或 JSON.load(请改用 JSON.parse)。
6.2.1. 全局加密上下文
全局加密上下文是默认使用的上下文,并像您的 application.rb 或环境配置文件中的其他配置属性一样进行配置。
config.active_record.encryption.key_provider = ActiveRecord::Encryption::EnvelopeEncryptionKeyProvider.new
config.active_record.encryption.encryptor = MyEncryptor.new
6.2.2. 按属性加密上下文
您可以通过在属性声明中传递参数来覆盖加密上下文参数:
class Attribute
encrypts :title, encryptor: MyAttributeEncryptor.new
end
6.2.3. 运行代码块时的加密上下文
您可以使用 ActiveRecord::Encryption.with_encryption_context 为给定代码块设置加密上下文:
ActiveRecord::Encryption.with_encryption_context(encryptor: ActiveRecord::Encryption::NullEncryptor.new) do
# ...
end
6.2.4. 内置加密上下文
6.2.4.1. 禁用加密
您可以运行不加密的代码:
ActiveRecord::Encryption.without_encryption do
# ...
end
这意味着读取加密文本将返回密文,并且保存的内容将以未加密的形式存储。
6.2.4.2. 保护加密数据
您可以运行不加密的代码,但要防止覆盖加密内容:
ActiveRecord::Encryption.protecting_encrypted_data do
# ...
end
如果您想在仍然对其运行任意代码(例如在 Rails 控制台中)时保护加密数据,这会很有用。