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 字节。如果您自己生成这些值,则应使用的最小长度为 12 字节的 primary key(将用于推导出 AES 32 字节密钥)和 20 字节的 salt。
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 时,每个字符最多可以占用 4 个字节,因此,可能在使用 UTF-8 的数据库中,列在字节数方面可以存储其大小的 4 倍。现在,加密的有效负载是作为 Base64 序列化的二进制字符串,因此它们可以存储在常规的 string
列中。因为它们是 ASCII 字节的序列,所以加密的列可以占用其明文版本的 4 倍。因此,即使存储在数据库中的字节相同,列也必须大 4 倍。
在实践中,这意味着
- 当加密使用西欧字母表(主要是 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("[email protected]") # You can query the model normally
除非您需要查询数据,否则建议使用非确定性方法。
在非确定性模式下,Active Record 使用 AES-GCM,密钥为 256 位,初始化向量为随机的。在确定性模式下,它也使用 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 夹具
您可以通过将此选项添加到您的 test.rb
中,让 Rails 夹具自动加密。
config.active_record.encryption.encrypt_fixtures = true
启用后,所有可加密属性都将根据模型中定义的加密设置进行加密。
3.2.1 Action Text 夹具
要加密 Action Text 夹具,您应将其放置在 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 Encryption 会尝试使用先前的加密方案,如果当前方案不起作用。
- 在查询确定性数据时,它将使用先前的方案添加密文,以便查询与使用不同方案加密的数据无缝协作。您必须设置
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 Encryption 尚未提供密钥轮换过程的自动管理。所有部分都已到位,但尚未实现。
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 Encryption 选项,如果您想在按环境的基础上设置它们。
建议使用 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:
选项提供的。
6.1.3 config.active_record.encryption.encrypt_fixtures
当为 true 时,fixtures 中的可加密属性将在加载时自动加密。默认值: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 控制台中)保护加密数据,这将非常方便。