lib/azure_blob/shared_key_signer.rb



# frozen_string_literal: true

require "base64"
require "openssl"
require_relative "canonicalized_headers"
require_relative "canonicalized_resource"

module AzureBlob
  class SharedKeySigner # :nodoc:
    def initialize(account_name:, access_key:, host:)
      @account_name = account_name
      @access_key = Base64.decode64(access_key)
      @host = host
      @remove_prefix = @host.include?("/#{@account_name}")
    end

    def authorization_header(uri:, verb:, headers: {})
      canonicalized_headers = CanonicalizedHeaders.new(headers)
      canonicalized_resource = CanonicalizedResource.new(uri, account_name)

      to_sign = [
        verb,
        *sanitize_headers(headers).fetch_values(
          :"Content-Encoding",
          :"Content-Language",
          :"Content-Length",
          :"Content-MD5",
          :"Content-Type",
          :"Date",
          :"If-Modified-Since",
          :"If-Match",
          :"If-None-Match",
          :"If-Unmodified-Since",
          :"Range"
        ) { nil },
        canonicalized_headers,
        canonicalized_resource,
      ].join("\n")

      "SharedKey #{account_name}:#{sign(to_sign)}"
    end

    def sas_token(uri, options = {})
      if remove_prefix
        uri = uri.clone
        uri.path = uri.path.delete_prefix("/#{account_name}")
      end

      to_sign = [
        options[:permissions],
        options[:start],
        options[:expiry],
        CanonicalizedResource.new(uri, account_name, url_safe: false, service_name: :blob),
        options[:identifier],
        options[:ip],
        options[:protocol],
        SAS::Version,
        SAS::Resources::Blob,
        nil,
        nil,
        nil,
        options[:content_disposition],
        nil,
        nil,
        options[:content_type],
      ].join("\n")

      query = {
        SAS::Fields::Permissions => options[:permissions],
        SAS::Fields::Version => SAS::Version,
        SAS::Fields::Expiry => options[:expiry],
        SAS::Fields::Resource => SAS::Resources::Blob,
        SAS::Fields::Disposition => options[:content_disposition],
        SAS::Fields::Type => options[:content_type],
        SAS::Fields::Signature => sign(to_sign),
      }.reject { |_, value| value.nil? }

      URI.encode_www_form(**query)
    end

    private

    def sign(body)
      Base64.strict_encode64(OpenSSL::HMAC.digest("sha256", access_key, body))
    end

    def sanitize_headers(headers)
      headers = headers.dup
      headers[:"Content-Length"] = nil if headers[:"Content-Length"].to_i == 0
      headers
    end

    module SAS # :nodoc:
      Version = "2024-05-04"
      module Fields # :nodoc:
        Permissions = :sp
        Version = :sv
        Expiry = :se
        Resource = :sr
        Signature = :sig
        Disposition = :rscd
        Type = :rsct
      end
      module Resources # :nodoc:
        Blob = :b
      end
    end

    attr_reader :access_key, :account_name, :remove_prefix
  end
end