# = Action Pack WebAuthn Assertion Response
#
# Handles the authenticator response from a WebAuthn authentication ceremony.
# When a user authenticates with an existing credential, the authenticator
# returns an assertion response containing a signature that proves possession
# of the private key.
#
# == Usage
#
#   # Look up the credential by ID
#   credential = user.credentials.find_by!(
#     credential_id: params[:id]
#   )
#
#   response = ActionPack::WebAuthn::Authenticator::AssertionResponse.new(
#     client_data_json: params[:response][:clientDataJSON],
#     authenticator_data: params[:response][:authenticatorData],
#     signature: params[:response][:signature],
#     credential: credential.to_public_key_credential,
#     origin: "https://example.com"
#   )
#
#   response.validate!
#
# == Validation
#
# In addition to the base Response validations, this class verifies:
#
# * The client data type is "webauthn.get"
# * The signature is valid for the credential's public key
#
class ActionPack::WebAuthn::Authenticator::AssertionResponse < ActionPack::WebAuthn::Authenticator::Response
  attr_reader :credential, :authenticator_data, :signature

  validate :client_data_type_must_be_get
  validate :signature_must_be_valid
  validate :sign_count_must_increase

  def initialize(credential:, authenticator_data:, signature:, **attributes)
    super(**attributes)
    @credential = credential
    @signature = signature
    @signature = Base64.urlsafe_decode64(@signature) unless @signature.encoding == Encoding::BINARY
    @authenticator_data = ActionPack::WebAuthn::Authenticator::Data.wrap(authenticator_data)
  rescue ArgumentError
    raise ActionPack::WebAuthn::InvalidResponseError, "Invalid base64 encoding in signature"
  end

  private
    def challenge_purpose
      "authentication"
    end

    def client_data_type_must_be_get
      unless client_data["type"] == "webauthn.get"
        errors.add(:base, "Client data type is not webauthn.get")
      end
    end

    def signature_must_be_valid
      client_data_hash = Digest::SHA256.digest(client_data_json)
      signed_data = authenticator_data.bytes.pack("C*") + client_data_hash
      digest = credential.public_key.oid == "ED25519" ? nil : "SHA256"

      unless credential.public_key.verify(digest, signature, signed_data)
        errors.add(:base, "Invalid signature")
      end
    rescue OpenSSL::PKey::PKeyError
      errors.add(:base, "Invalid signature")
    end

    def sign_count_must_increase
      unless sign_count_increased?
        errors.add(:base, "Sign count did not increase")
      end
    end

    def sign_count_increased?
      if authenticator_data.sign_count.zero? && credential.sign_count.zero?
        # Some authenticators always return 0 for the sign count, even after multiple authentications.
        # In that case, we have to check that both the stored and returned sign counts are 0,
        # which indicates that the authenticator is likely not updating the sign count.
        true
      else
        authenticator_data.sign_count > credential.sign_count
      end
    end
end
