跳至内容 跳至搜索

Active Support 消息加密器

MessageEncryptor 是一种简单的方法,可以加密存储在不信任位置的值。

密文和初始化向量将被 base64 编码并返回给您。

这可用于类似于 MessageVerifier 的情况,但您不希望用户能够确定有效负载的值。

len   = ActiveSupport::MessageEncryptor.key_len
salt  = SecureRandom.random_bytes(len)
key   = ActiveSupport::KeyGenerator.new('password').generate_key(salt, len) # => "\x89\xE0\x156\xAC..."
crypt = ActiveSupport::MessageEncryptor.new(key)                            # => #<ActiveSupport::MessageEncryptor ...>
encrypted_data = crypt.encrypt_and_sign('my secret data')                   # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..."
crypt.decrypt_and_verify(encrypted_data)                                    # => "my secret data"

如果提供的数据无法解密或验证,则 decrypt_and_verify 方法将引发 ActiveSupport::MessageEncryptor::InvalidMessage 异常。

crypt.decrypt_and_verify('not encrypted data') # => ActiveSupport::MessageEncryptor::InvalidMessage

将消息限制为特定目的

默认情况下,任何消息都可以在您的应用程序中使用。但它们也可以限制为特定的 :purpose

token = crypt.encrypt_and_sign("this is the chair", purpose: :login)

然后,必须在验证时传递相同的目的才能获取数据。

crypt.decrypt_and_verify(token, purpose: :login)    # => "this is the chair"
crypt.decrypt_and_verify(token, purpose: :shipping) # => nil
crypt.decrypt_and_verify(token)                     # => nil

同样,如果消息没有目的,则在使用特定目的进行验证时不会返回该消息。

token = crypt.encrypt_and_sign("the conversation is lively")
crypt.decrypt_and_verify(token, purpose: :scare_tactics) # => nil
crypt.decrypt_and_verify(token)                          # => "the conversation is lively"

使消息过期

默认情况下,消息将永远持续存在,并且在一年后验证仍将返回原始值。但可以将消息设置为使用 :expires_in:expires_at 在给定时间过期。

crypt.encrypt_and_sign(parcel, expires_in: 1.month)
crypt.encrypt_and_sign(doowad, expires_at: Time.now.end_of_year)

然后,可以验证和返回消息,直到过期时间。此后,验证将返回 nil

轮换密钥

MessageEncryptor 还支持通过回退到加密器堆栈来轮换旧配置。调用 rotate 来构建并添加一个加密器,这样 decrypt_and_verify 也会尝试回退。

默认情况下,任何轮换加密器都使用主加密器的值,除非另有指定。

您将为加密器提供新的默认值

crypt = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm")

然后,通过将旧值添加为回退来逐步轮换旧值。使用旧值生成的任何消息都将一直有效,直到轮换被移除。

crypt.rotate old_secret            # Fallback to an old secret instead of @secret.
crypt.rotate cipher: "aes-256-cbc" # Fallback to an old cipher instead of aes-256-gcm.

但是,如果密钥和密码同时更改,则应将上述操作合并为

crypt.rotate old_secret, cipher: "aes-256-cbc"
命名空间
方法
D
E
K
N

常量

OpenSSLCipherError = OpenSSL::Cipher::CipherError
 

类公共方法

key_len(cipher = default_cipher)

给定一个密码,返回密码的密钥长度,以帮助生成所需大小的密钥

# File activesupport/lib/active_support/message_encryptor.rb, line 252
def self.key_len(cipher = default_cipher)
  OpenSSL::Cipher.new(cipher).key_len
end

new(secret, sign_secret = nil, **options)

初始化一个新的 MessageEncryptorsecret 必须至少与密码密钥大小一样长。对于默认的 ‘aes-256-gcm’ 密码,这为 256 位。如果您使用的是用户输入的密钥,则可以使用 ActiveSupport::KeyGenerator 或类似的密钥派生函数来生成合适的密钥。

第一个附加参数用作 MessageVerifier 的签名密钥。这允许您指定加密和签名数据的密钥。在使用像 ‘aes-256-gcm’ 这样的 AEAD 密码时会被忽略。

ActiveSupport::MessageEncryptor.new('secret', 'signature_secret')

选项

:cipher

要使用的密码。可以是 OpenSSL::Cipher.ciphers 返回的任何密码。默认值为 ‘aes-256-gcm’。

:digest

Digest 用于签名。在使用像 ‘aes-256-gcm’ 这样的 AEAD 密码时会被忽略。

:serializer

用于序列化消息数据的序列化器。您可以指定任何响应 dumpload 的对象,也可以从几个预配置的序列化器中选择::marshal:json_allow_marshal:json:message_pack_allow_marshal:message_pack

预配置的序列化器包括一个回退机制,以支持多种反序列化格式。例如,:marshal 序列化器将使用 Marshal 进行序列化,但可以使用 MarshalActiveSupport::JSONActiveSupport::MessagePack 进行反序列化。这使得在序列化器之间进行迁移变得容易。

:marshal:json_allow_marshal:message_pack_allow_marshal 序列化器支持使用 Marshal 进行反序列化,但其他序列化器不支持。请注意,Marshal 是在消息签名密钥泄露的情况下进行反序列化攻击的潜在媒介。如果可能,请选择不支持 Marshal 的序列化器。

:message_pack:message_pack_allow_marshal 序列化器使用 ActiveSupport::MessagePack,它可以对 JSON 不支持的某些 Ruby 类型进行往返,并且可能提供更好的性能。但是,这些需要 msgpack gem。

在使用 Rails 时,默认值取决于 config.active_support.message_serializer。否则,默认值为 :marshal

:url_safe

默认情况下,MessageEncryptor 生成符合 RFC 4648 的字符串,这些字符串不是 URL 安全的。换句话说,它们可能包含“+”和“/”。如果您想生成 URL 安全字符串(符合 RFC 4648 中的“具有 URL 和文件名安全字母表的 Base 64 编码”),则可以传递 true

:force_legacy_metadata_serializer

是否使用旧的元数据序列化器,该序列化器先序列化消息,然后将其包装在也经过序列化的信封中。这是 Rails 7.0 及以下版本的默认值。

如果您没有传递真值,则默认值将使用 config.active_support.use_message_serializer_for_metadata 设置。

# File activesupport/lib/active_support/message_encryptor.rb, line 183
def initialize(secret, sign_secret = nil, **options)
  super(**options)
  @secret = secret
  @cipher = options[:cipher] || self.class.default_cipher
  @aead_mode = new_cipher.authenticated?
  @verifier = if !@aead_mode
    MessageVerifier.new(sign_secret || secret, **options, serializer: NullSerializer)
  end
end

实例公共方法

decrypt_and_verify(message, **options)

解密并验证消息。我们需要验证消息以避免填充攻击。参考:www.limited-entropy.com/padding-oracle-attacks/。

选项

:purpose

生成消息的目的。如果目的不匹配,decrypt_and_verify 将返回 nil

message = encryptor.encrypt_and_sign("hello", purpose: "greeting")
encryptor.decrypt_and_verify(message, purpose: "greeting") # => "hello"
encryptor.decrypt_and_verify(message)                      # => nil

message = encryptor.encrypt_and_sign("bye")
encryptor.decrypt_and_verify(message)                      # => "bye"
encryptor.decrypt_and_verify(message, purpose: "greeting") # => nil
# File activesupport/lib/active_support/message_encryptor.rb, line 241
def decrypt_and_verify(message, **options)
  catch_and_raise :invalid_message_format, as: InvalidMessage do
    catch_and_raise :invalid_message_serialization, as: InvalidMessage do
      catch_and_ignore :invalid_message_content do
        read_message(message, **options)
      end
    end
  end
end

encrypt_and_sign(value, **options)

加密并签名消息。我们需要签名消息以避免填充攻击。参考:www.limited-entropy.com/padding-oracle-attacks/。

选项

:expires_at

消息过期的日期时间。在此日期时间之后,消息的验证将失败。

message = encryptor.encrypt_and_sign("hello", expires_at: Time.now.tomorrow)
encryptor.decrypt_and_verify(message) # => "hello"
# 24 hours later...
encryptor.decrypt_and_verify(message) # => nil
:expires_in

消息有效的持续时间。在此持续时间过后,消息的验证将失败。

message = encryptor.encrypt_and_sign("hello", expires_in: 24.hours)
encryptor.decrypt_and_verify(message) # => "hello"
# 24 hours later...
encryptor.decrypt_and_verify(message) # => nil
:purpose

消息的目的。如果指定,则在验证消息时必须指定相同的目的;否则,验证将失败。(参见 decrypt_and_verify。)

# File activesupport/lib/active_support/message_encryptor.rb, line 220
def encrypt_and_sign(value, **options)
  create_message(value, **options)
end