跳至内容 跳至搜索

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