# frozen_string_literal: true require "active_support/messages/rotation_coordinator" module ActiveSupport class MessageEncryptors < Messages::RotationCoordinator ## # :attr_accessor: transitional # # If true, the first two rotation option sets are swapped when building # message encryptors. For example, with the following configuration, message # encryptors will encrypt messages using serializer: Marshal, url_safe: true, # and will able to decrypt messages that were encrypted using any of the # three option sets: # # encryptors = ActiveSupport::MessageEncryptors.new { ... } # encryptors.rotate(serializer: JSON, url_safe: true) # encryptors.rotate(serializer: Marshal, url_safe: true) # encryptors.rotate(serializer: Marshal, url_safe: false) # encryptors.transitional = true # # This can be useful when performing a rolling deploy of an application, # wherein servers that have not yet been updated must still be able to # decrypt messages from updated servers. In such a scenario, first perform a # rolling deploy with the new rotation (e.g. serializer: JSON, url_safe: true) # as the first rotation and transitional = true. Then, after all # servers have been updated, perform a second rolling deploy with # transitional = false. ## # :method: initialize # :call-seq: initialize(&secret_generator) # # Initializes a new instance. +secret_generator+ must accept a salt and a # +secret_length+ kwarg, and return a suitable secret (string) or secrets # (array of strings). +secret_generator+ may also accept other arbitrary # kwargs. If #rotate is called with any options matching those kwargs, those # options will be passed to +secret_generator+ instead of to the message # encryptor. # # encryptors = ActiveSupport::MessageEncryptors.new do |salt, secret_length:, base:| # MySecretGenerator.new(base).generate(salt, secret_length) # end # # encryptors.rotate(base: "...") ## # :method: [] # :call-seq: [](salt) # # Returns a MessageEncryptor configured with a secret derived from the # given +salt+, and options from #rotate. MessageEncryptor instances will # be memoized, so the same +salt+ will return the same instance. ## # :method: []= # :call-seq: []=(salt, encryptor) # # Overrides a MessageEncryptor instance associated with a given +salt+. ## # :method: rotate # :call-seq: # rotate(**options) # rotate(&block) # # Adds +options+ to the list of option sets. Messages will be encrypted # using the first set in the list. When decrypting, however, each set will # be tried, in order, until one succeeds. # # Notably, the +:secret_generator+ option can specify a different secret # generator than the one initially specified. The secret generator must # respond to +call+, accept a salt and a +secret_length+ kwarg, and return # a suitable secret (string) or secrets (array of strings). The secret # generator may also accept other arbitrary kwargs. # # If any options match the kwargs of the operative secret generator, those # options will be passed to the secret generator instead of to the message # encryptor. # # For fine-grained per-salt rotations, a block form is supported. The block # will receive the salt, and should return an appropriate options Hash. The # block may also return +nil+ to indicate that the rotation does not apply # to the given salt. For example: # # encryptors = ActiveSupport::MessageEncryptors.new { ... } # # encryptors.rotate do |salt| # case salt # when :foo # { serializer: JSON, url_safe: true } # when :bar # { serializer: Marshal, url_safe: true } # end # end # # encryptors.rotate(serializer: Marshal, url_safe: false) # # # Uses `serializer: JSON, url_safe: true`. # # Falls back to `serializer: Marshal, url_safe: false`. # encryptors[:foo] # # # Uses `serializer: Marshal, url_safe: true`. # # Falls back to `serializer: Marshal, url_safe: false`. # encryptors[:bar] # # # Uses `serializer: Marshal, url_safe: false`. # encryptors[:baz] ## # :method: rotate_defaults # :call-seq: rotate_defaults # # Invokes #rotate with the default options. ## # :method: clear_rotations # :call-seq: clear_rotations # # Clears the list of option sets. ## # :method: on_rotation # :call-seq: on_rotation(&callback) # # Sets a callback to invoke when a message is decrypted using an option set # other than the first. # # For example, this callback could log each time it is called, and thus # indicate whether old option sets are still in use or can be removed from # rotation. ## private def build(salt, secret_generator:, secret_generator_options:, **options) secret_length = MessageEncryptor.key_len(*options[:cipher]) secret = secret_generator.call(salt, secret_length: secret_length, **secret_generator_options) MessageEncryptor.new(*Array(secret), **options) end end end