跳到內容 跳到搜尋

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)

針對指定的密文,傳回密文的 key 長度,以幫助產生所需大小的 key

# 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 的長度必須至少與密文 key 長度相同。針對預設的「aes-256-gcm」密文,長度為 256 位元。如果您正在使用使用者輸入的 secret,您可以使用 ActiveSupport::KeyGenerator 或類似的 key 衍生函數來產生合適的 key。

第一個額外的參數用作 MessageVerifier 的簽章 key。這允許您指定 key 來加密和簽署資料。在使用「aes-256-gcm」等 AEAD 密文時會略過此參數。

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

選項

:cipher

要使用的密文。它可以是 OpenSSL::Cipher.ciphers 傳回的任何密文。預設為「aes-256-gcm」。

:digest

用於簽章的 Digest。在使用「aes-256-gcm」等 AEAD 密文時會略過此參數。

:serializer

<code>dump</code> 和 <code>load</code> 響應的序列化程式用於序列化訊息資料。您可指定任一物件,或從下列預先設定好的序列化程式中進行選擇:<code>:marshal</code>、<code>:json_allow_marshal</code>、<code>:json</code>、<code>:message_pack_allow_marshal</code>、<code>:message_pack</code>。

預先設定的序列化程式包含支援多重反序列化格式的退避機制。例如,<code>:marshal</code> 序列化程式會使用 <code>Marshal</code> 進行序列化,但可使用 <code>Marshal</code>、 <code>ActiveSupport::JSON</code><code>ActiveSupport::MessagePack</code> 進行反序列化。這可輕易地在序列化程式間進行轉移。

<code>:marshal</code>、<code>:json_allow_marshal</code> 和 <code>:message_pack_allow_marshal</code> 序列化程式支援使用 <code>Marshal</code> 進行反序列化,但其他序列化程式則不支援。要注意的是,在訊息簽名機密外洩時,<code>Marshal</code> 是反序列化攻擊的潛在媒介。若情況許可,請選擇不支援 <code>Marshal</code> 的序列化程式。

<code>:message_pack</code> 和 <code>:message_pack_allow_marshal</code> 序列化程式使用 <code>ActiveSupport::MessagePack</code>,其可往復 (Round-trip) 一些不受 <code>JSON</code> 支援的 Ruby 型別,並提供更佳的效能。不過,這些型別需要使用 <code>msgpack</code> gem。

在使用 Rails 時,預設值取決於 <code>config.active_support.message_serializer</code>。否則,預設值為 <code>:marshal</code>。

:url_safe

預設情況下,<code>MessageEncryptor</code> 會產生相符 RFC 4648 標準的字串,這些字串不符合 URL 安全原則。換言之,它們可能包含「+」和「/」。若要產生符合 URL 安全原則的字串(符合 RFC 4648 中的「使用 URL 和安全檔案名稱符號組進行 Base 64 編碼」),您可以傳遞 <code>true</code>。

:force_legacy_metadata_serializer

是否使用舊版的 metadata 序列化程式,其會先將訊息序列化,再將其封裝在也已序列化的信封中。此機制為 Rails 7.0 及以下版本的預設值。

如果您未傳遞 true 值,則預設值將使用 <code>config.active_support.use_message_serializer_for_metadata</code> 設定。

# 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

產生訊息所用的目的。若目的不相符,<code>decrypt_and_verify</code> 將會傳回 <code>nil</code>。

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