1
0

ActiveStorage#signed_idの生成を読解

Posted at

発生事象

複数のstg環境でActiveStorage#signed_idが同じものを生成したのでロジックを追ってみた

ActiveStorage#signed_idの生成を読解

signed_idはActiveStorageのBlobのidを暗号化したもの

エントリーポイント

ActiveStorage::Blob#signed_idがエントリーポイントです。

  def signed_id(purpose: :blob_id, expires_in: nil, expires_at: nil)
    super
  end

superの実体

ActiveRecord::SignedId#signed_idが呼ばれます。

    def signed_id(expires_in: nil, expires_at: nil, purpose: nil)
      raise ArgumentError, "Cannot get a signed_id for a new record" if new_record?

      self.class.signed_id_verifier.generate id, expires_in: expires_in, expires_at: expires_at, purpose: self.class.combine_signed_id_purposes(purpose)
    end

signed_id_verifier

利用されるMessageVerifierはActiveStorage.verifierです。

(下位互換のためActiveRecord::SignedId.signed_id_verifierではなく上記を利用するようにoverrideしています)

    def signed_id_verifier 
      @signed_id_verifier ||= ActiveStorage.verifier
    end

ActiveStorage.verifier

rails/activestorage/lib/active_storage/engine.rbで設定されています。

    initializer "active_storage.verifier" do
      config.after_initialize do |app|
        ActiveStorage.verifier = app.message_verifier("ActiveStorage")
      end
    end

app.message_verifier

rails/railties/lib/rails/application.rb
message_verifier

message_verifiers

    def message_verifier(verifier_name)
      message_verifiers[verifier_name]
    end
    def message_verifiers
      @message_verifiers ||=
        ActiveSupport::MessageVerifiers.new do |salt, secret_key_base: self.secret_key_base|
          key_generator(secret_key_base).generate_key(salt)
        end.rotate_defaults
    end

ActiveSupport::MessageVerifiers

ActiveSupport::MessageVerifiersへのアクセスするキーはsaltとして扱われます

    def [](salt)
      @codecs[salt] ||= build_with_rotations(salt)
    end

ActiveSupport::MessageVerifiersの各verifierの生成

build_with_rotationsの実装

        def build_with_rotations(salt)
          rotate_options = @rotate_options.map { |options| options.is_a?(Proc) ? options.(salt) : options }
          transitional = self.transitional && rotate_options.first
          rotate_options.compact!
          rotate_options[0..1] = rotate_options[0..1].reverse if transitional
          rotate_options = rotate_options.map { |options| normalize_options(options) }.uniq

          raise "No options have been configured for #{salt}" if rotate_options.empty?

          rotate_options.map { |options| build(salt.to_s, **options) }.reduce(&:fall_back_to)
        end

        def build(salt, secret_generator:, secret_generator_options:, **options)
          raise NotImplementedError
        end

buildの実体

rails/activesupport/lib/active_support/message_verifiers.rbbuild を上書きしています

      def build(salt, secret_generator:, secret_generator_options:, **options)
        MessageVerifier.new(secret_generator.call(salt, **secret_generator_options), **options)
      end

secret_generatorは上記message_verifiersの生成ロジックで指定しているため、key_generator(secret_key_base).generate_key(salt)で生成されます

secret_key_base

暗号化キーを生成するためのキーは
rails/railties/lib/rails/application.rbsecret_key_base

    def secret_key_base
  if Rails.env.local? || ENV["SECRET_KEY_BASE_DUMMY"]
    config.secret_key_base ||= generate_local_secret
  else
    validate_secret_key_base(
      ENV["SECRET_KEY_BASE"] || credentials.secret_key_base
    )
  end
end

結論

ENV["SECRET_KEY_BASE"] || credentials.secret_key_base が環境間で同じ値であれば、同じidに対して同じActiveStorage::Blob#signed_idが生成される

再現

signed_id_verifierの生成

secret=Rails.application.key_generator.generate_key("ActiveStorage")
signed_id_verifier=ActiveSupport::MessageVerifier.new secret

signed_idの生成

id = {ActiveStorage::Blob.id}
signed_id = signed_id_verifier.generate(id, purpose: :blob_id)

signed_idの検証

signed_id_verifier.verify(signed_id, purpose: :blob_id)
# => {ActiveStorage::Blob.id}
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0