class ActiveSupport::MessageEncryptor
crypt.rotate old_secret, cipher: “aes-256-cbc”
the above should be combined into:
Though if both the secret and the cipher was changed at the same time,
crypt.rotate cipher: “aes-256-cbc” # Fallback to an old cipher instead of aes-256-gcm.
crypt.rotate old_secret # Fallback to an old secret instead of @secret.
generated with the old values will then work until the rotation is removed.
Then gradually rotate the old values out by adding them as fallbacks. Any message
crypt = ActiveSupport::MessageEncryptor.new(@secret, cipher: “aes-256-gcm”)
You’d give your encryptor the new defaults:
encryptor unless specified otherwise.
By default any rotated encryptors use the values of the primary
so decrypt_and_verify
will also try the fallback.
back to a stack of encryptors. Call rotate
to build and add an encryptor
MessageEncryptor also supports rotating out old configurations by falling
=== Rotating keys
Thereafter, verifying returns nil
.
Then the messages can be verified and returned up to the expire time.
crypt.encrypt_and_sign(doowad, expires_at: Time.now.end_of_year)
crypt.encrypt_and_sign(parcel, expires_in: 1.month)
time with :expires_in
or :expires_at
.
return the original value. But messages can be set to expire at a given
By default messages last forever and verifying one year from now will still
=== Making messages expire
crypt.decrypt_and_verify(token) # => “the conversation is lively”
crypt.decrypt_and_verify(token, purpose: :scare_tactics) # => nil
token = crypt.encrypt_and_sign(“the conversation is lively”)
a specific purpose.
Likewise, if a message has no purpose it won’t be returned when verifying with
crypt.decrypt_and_verify(token) # => nil
crypt.decrypt_and_verify(token, purpose: :shipping) # => nil
crypt.decrypt_and_verify(token, purpose: :login) # => “this is the chair”
Then that same purpose must be passed when verifying to get the data back out:
token = crypt.encrypt_and_sign(“this is the chair”, purpose: :login)
confined to a specific :purpose
.
By default any message can be used throughout your app. But they can also be
=== Confining messages to a specific purpose
crypt.decrypt_and_verify(encrypted_data) # => “my secret data”
encrypted_data = crypt.encrypt_and_sign(‘my secret data’) # => “NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh…”
crypt = ActiveSupport::MessageEncryptor.new(key) # => #<ActiveSupport::MessageEncryptor …>
key = ActiveSupport::KeyGenerator.new(‘password’).generate_key(salt, len) # => “x89xE0x156xAC…”
salt = SecureRandom.random_bytes(len)
len = ActiveSupport::MessageEncryptor.key_len
where you don’t want users to be able to determine the value of the payload.
This can be used in situations similar to the MessageVerifier
, but
to you.
The cipher text and initialization vector are base64 encoded and returned
somewhere you don’t trust.
MessageEncryptor is a simple way to encrypt values which get stored
def self.key_len(cipher = default_cipher)
def self.key_len(cipher = default_cipher) OpenSSL::Cipher.new(cipher).key_len end
def _decrypt(encrypted_message, purpose)
def _decrypt(encrypted_message, purpose) cipher = new_cipher encrypted_data, iv, auth_tag = encrypted_message.split("--").map { |v| ::Base64.strict_decode64(v) } # Currently the OpenSSL bindings do not raise an error if auth_tag is # truncated, which would allow an attacker to easily forge it. See # https://github.com/ruby/openssl/issues/63 raise InvalidMessage if aead_mode? && (auth_tag.nil? || auth_tag.bytes.length != 16) cipher.decrypt cipher.key = @secret cipher.iv = iv if aead_mode? cipher.auth_tag = auth_tag cipher.auth_data = "" end decrypted_data = cipher.update(encrypted_data) decrypted_data << cipher.final message = Messages::Metadata.verify(decrypted_data, purpose) @serializer.load(message) if message rescue OpenSSLCipherError, TypeError, ArgumentError raise InvalidMessage end
def _encrypt(value, **metadata_options)
def _encrypt(value, **metadata_options) cipher = new_cipher cipher.encrypt cipher.key = @secret # Rely on OpenSSL for the initialization vector iv = cipher.random_iv cipher.auth_data = "" if aead_mode? encrypted_data = cipher.update(Messages::Metadata.wrap(@serializer.dump(value), metadata_options)) encrypted_data << cipher.final blob = "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}" blob = "#{blob}--#{::Base64.strict_encode64 cipher.auth_tag}" if aead_mode? blob end
def aead_mode?
def aead_mode? @aead_mode ||= new_cipher.authenticated? end
def decrypt_and_verify(data, purpose: nil, **)
Decrypt and verify a message. We need to verify the message in order to
def decrypt_and_verify(data, purpose: nil, **) _decrypt(verifier.verify(data), purpose) end
def default_cipher #:nodoc:
def default_cipher #:nodoc: if use_authenticated_message_encryption "aes-256-gcm" else "aes-256-cbc" end end
def encrypt_and_sign(value, expires_at: nil, expires_in: nil, purpose: nil)
Encrypt and sign a message. We need to sign the message in order to avoid
def encrypt_and_sign(value, expires_at: nil, expires_in: nil, purpose: nil) verifier.generate(_encrypt(value, expires_at: expires_at, expires_in: expires_in, purpose: purpose)) end
def initialize(secret, *signature_key_or_options)
+SHA1+. Ignored when using an AEAD cipher like 'aes-256-gcm'.
* :digest - String of digest to use for signing. Default is
OpenSSL::Cipher.ciphers. Default is 'aes-256-gcm'.
* :cipher - Cipher to use. Can be any cipher returned by
Options:
ActiveSupport::MessageEncryptor.new('secret', 'signature_secret')
This allows you to specify keys to encrypt and sign data.
First additional parameter is used as the signature key for +MessageVerifier+.
derivation function.
key by using ActiveSupport::KeyGenerator or a similar key
bits. If you are using a user-entered secret, you can generate a suitable
the cipher key size. For the default 'aes-256-gcm' cipher, this is 256
Initialize a new MessageEncryptor. +secret+ must be at least as long as
def initialize(secret, *signature_key_or_options) options = signature_key_or_options.extract_options! sign_secret = signature_key_or_options.first @secret = secret @sign_secret = sign_secret @cipher = options[:cipher] || self.class.default_cipher @digest = options[:digest] || "SHA1" unless aead_mode? @verifier = resolve_verifier @serializer = options[:serializer] || Marshal end
def new_cipher
def new_cipher OpenSSL::Cipher.new(@cipher) end
def resolve_verifier
def resolve_verifier if aead_mode? NullVerifier else MessageVerifier.new(@sign_secret || @secret, digest: @digest, serializer: NullSerializer) end end