ActiveStorage#signed_idの生成を読解

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

# ActiveStorage#signed_idの生成を読解

[signed_id](https://api.rubyonrails.org/v7.1.3/classes/ActiveStorage/Blob.html#method-i-signed_id)はActiveStorageのBlobの`id`を暗号化したもの

## エントリーポイント
[`ActiveStorage::Blob#signed_id`](https://github.com/rails/rails/blob/36c1591bcb5e0ee3084759c7f42a706fe5bb7ca7/activestorage/app/models/active_storage/blob.rb#L163-L165)がエントリーポイントです。
```ruby
  def signed_id(purpose: :blob_id, expires_in: nil, expires_at: nil)
    super
  end
```
## superの実体
[`ActiveRecord::SignedId#signed_id`](https://github.com/rails/rails/blob/36c1591bcb5e0ee3084759c7f42a706fe5bb7ca7/activerecord/lib/active_record/signed_id.rb#L112-L116)が呼ばれます。

```ruby
    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`](https://github.com/rails/rails/blob/36c1591bcb5e0ee3084759c7f42a706fe5bb7ca7/activestorage/app/models/active_storage/blob.rb#L134-L136)です。

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

```ruby
    def signed_id_verifier 
      @signed_id_verifier ||= ActiveStorage.verifier
    end
```

## ActiveStorage.verifier
[rails/activestorage/lib/active_storage/engine.rb](https://github.com/rails/rails/blob/36c1591bcb5e0ee3084759c7f42a706fe5bb7ca7/activestorage/lib/active_storage/engine.rb#L139-L143)で設定されています。

```ruby
    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](https://github.com/rails/rails/blob/main/railties/lib/rails/application.rb#L221-L223)
と
[message_verifiers](https://github.com/rails/rails/blob/main/railties/lib/rails/application.rb#L196-L201)
```ruby
    def message_verifier(verifier_name)
      message_verifiers[verifier_name]
    end
```

```ruby
    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](https://github.com/rails/rails/blob/main/activesupport/lib/active_support/messages/rotation_coordinator.rb#L18-L20)として扱われます

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

## ActiveSupport::MessageVerifiersの各verifierの生成

[build_with_rotations](https://github.com/rails/rails/blob/main/activesupport/lib/active_support/messages/rotation_coordinator.rb#L76-L90)の実装

```ruby
        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.rb` で [`build` を上書きしています](https://github.com/rails/rails/blob/main/activesupport/lib/active_support/message_verifiers.rb#L131-L133)

```ruby
      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.rb` の[secret_key_base](https://github.com/rails/rails/blob/main/railties/lib/rails/application.rb#L459C9-L4670)

```ruby
    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の生成
```ruby
secret=Rails.application.key_generator.generate_key("ActiveStorage")
signed_id_verifier=ActiveSupport::MessageVerifier.new secret
```

### signed_idの生成
```ruby
id = {ActiveStorage::Blob.id}
signed_id = signed_id_verifier.generate(id, purpose: :blob_id)
```

### signed_idの検証
```ruby
signed_id_verifier.verify(signed_id, purpose: :blob_id)
# => {ActiveStorage::Blob.id}
```