DiffDaily

Deep & Concise - OSS変更の定点観測

[rails/rails] ActiveStorageのチェックサム計算責務をServiceクラスに移譲

rails/rails

背景

これまでActiveStorageでは、ファイルの整合性チェックに使用するチェックサムの計算が複数のクラスに分散していました。具体的にはActiveStorage::Blobクラスが内部でOpenSSL::Digest::MD5を直接使用してチェックサムを計算し、ActiveStorage::Downloaderクラスが検証を担当するという構造でした。

しかし、S3、GCS、Azureといった主要なクラウドストレージサービスは、MD5以外にもSHA256などの複数のチェックサムアルゴリズムをサポートしています。特にFIPS準拠が求められる環境ではMD5が使用できないため、より柔軟なアルゴリズム選択が必要とされていました。

#54468では、チェックサムの計算と検証の責務を完全にActiveStorage::Serviceクラスに集約することで、各ストレージサービスが独自のアルゴリズムを実装できるようにしました。

主な変更内容

1. Serviceクラスへのチェックサム計算メソッドの追加

ActiveStorage::Service基底クラスに、チェックサム計算を担当する2つの新しいメソッドが追加されました。

def compute_checksum(io, **options)
  raise ArgumentError, "io must be rewindable" unless io.respond_to?(:rewind)

  # Fileオブジェクトの場合はDigestクラスのfile実装を使用
  return checksum_implementation(**options).file(io).base64digest if io.is_a?(File)
  return checksum_implementation(**options).base64digest(io.read) if default_chunk_size.to_i == 0

  checksum_implementation(**options).new.tap do |checksum|
    read_buffer = "".b
    while io.read(default_chunk_size, read_buffer)
      checksum << read_buffer
    end

    io.rewind
  end.base64digest
end

def checksum_implementation(**)
  OpenSSL::Digest::MD5
end

compute_checksumメソッドは、IOオブジェクトからチャンク単位でデータを読み込みながらチェックサムを計算します。デフォルトのチャンクサイズは5MBです。checksum_implementationメソッドは使用するダイジェストクラスを返し、デフォルトではMD5を使用します。

2. S3ServiceでのSHA256サポート

S3Serviceクラスでは、初期化時にdefault_digest_typeオプションで使用するアルゴリズムを指定できるようになりました。

def initialize(bucket:, upload: {}, public: false, default_digest_type: :md5, **options)
  @client = Aws::S3::Resource.new(**options)
  @transfer_manager = Aws::S3::TransferManager.new(client: @client.client) if defined?(Aws::S3::TransferManager)
  @bucket = @client.bucket(bucket)

  @default_digest_type = default_digest_type.downcase.to_sym
  @default_digest_class = digest_to_class(@default_digest_type)
  # ...
end

def compute_checksum(file, **options)
  default_digest_type == :md5 ? super : "#{default_digest_type}:#{super}"
end

SHA256を使用する場合、チェックサムの形式は"sha256:base64digest"となり、プレフィックスでアルゴリズムを識別できます。

設定ファイルでの指定例:

amazon:
  service: S3
  access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
  secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
  region: us-east-1
  bucket: your_bucket
  default_digest_type: :sha256

3. ダイレクトアップロードのアルゴリズム対応

クライアント側のJavaScriptでもSHA256をサポートするため、新しいアルゴリズムモジュールが追加されました。

import { sha256 } from "js-sha256"

function hexToBase64(hexString) {
  const bytes = []
  for (let i = 0; i < hexString.length; i += 2) {
    bytes.push(parseInt(hexString.substring(i, i + 2), 16))
  }

  let binary = ""
  for (let i = 0; i < bytes.length; i++) {
    binary += String.fromCharCode(bytes[i])
  }

  return btoa(binary)
}

export const sha256Algorithm = {
  createBuffer: () => sha256.create(),
  append: (buffer, data) => buffer.update(data),
  getChecksum: (buffer) => {
    const hexDigest = buffer.hex()
    return `sha256:${hexToBase64(hexDigest)}`
  }
}

フォームヘルパーでアルゴリズムを指定:

<%= form.file_field :attachment, 
    direct_upload: true, 
    data_checksum_algorithm: "sha256" %>

生成されるHTML:

<input data-direct-upload-url="http://example.com/rails/active_storage/direct_uploads" 
       data-checksum-algorithm="sha256" 
       type="file" 
       name="post[attachment]" 
       id="post_attachment" />

4. Downloaderクラスの削除とServiceへの統合

ActiveStorage::Downloaderクラスが削除され、その機能がService基底クラスのopenメソッドに統合されました。これにより、ダウンロードと検証のロジックが各サービスで制御できるようになりました。

変更前:

ActiveStorage::Downloader.new(service).open(key, checksum: checksum)

変更後:

service.open(key, checksum: checksum)

内部的にはdownload_and_verify_tempfileプライベートメソッドが処理を担当し、サービスのcompute_checksumメソッドを使用して検証を行います。

5. Blobクラスからチェックサム計算ロジックの削除

ActiveStorage::Blobクラスからcompute_checksum_in_chunksプライベートメソッドが削除され、サービスのcompute_checksumメソッドを呼び出すようになりました。

def unfurl(io, identify: true)
  self.checksum = service&.compute_checksum(io)
  self.content_type = extract_content_type(io) if content_type.nil? || identify
  self.byte_size = io.size
  self.identified = true
end

技術的な影響

後方互換性

  • デフォルトではMD5を使用するため、既存のアプリケーションへの影響はありません
  • S3Serviceでdefault_digest_type: :sha256を指定した場合のみSHA256が使用されます
  • 既存のMD5チェックサムを持つBlobは引き続き正常に動作します

パフォーマンス

  • チャンクサイズは5MBに設定されており、メモリ効率的にチェックサムを計算できます
  • Fileオブジェクトの場合はDigest.file()メソッドを使用し、最適化されたパスを利用します

セキュリティ

  • FIPS準拠環境でSHA256を使用できるようになりました
  • 各クラウドサービスが推奨するアルゴリズムを選択可能になりました

まとめ

この変更により、ActiveStorageのチェックサム計算の責務が完全にServiceクラスに集約されました。各ストレージサービスが独自のアルゴリズムを実装でき、MD5以外の選択肢が提供されるようになりました。特にS3ServiceでのSHA256サポートは、セキュリティ要件が厳格な環境での利用を可能にする重要な機能追加です。

この変更は、将来的にGCSやAzureサービスでも同様のアルゴリズム選択をサポートするための基盤となります。