# frozen_string_literal: true

module U2F
  class U2F
    attr_accessor :app_id
    ##
    # * *Args*:
    #   - +app_id+:: An application (facet) ID string
    #
    def initialize(app_id)
      @app_id = app_id
    end

    ##
    # Generate data to be sent to the U2F device before authenticating
    #
    # * *Args*:
    #   - +key_handles+:: +Array+ of previously registered U2F key handles
    #
    # * *Returns*:
    #   - An +Array+ of +SignRequest+ objects
    #
    def authentication_requests(key_handles)
      key_handles = [key_handles] unless key_handles.is_a? Array
      key_handles.map do |key_handle|
        SignRequest.new(key_handle)
      end
    end

    ##
    # Authenticate a response from the U2F device
    #
    # * *Args*:
    #   - +challenge+:: Challenge string
    #   - +response+:: Response from the U2F device as a +SignResponse+ object
    #   - +registration_public_key+:: Public key of the registered U2F device as binary string
    #   - +registration_counter+:: +Integer+ with the current counter value of the registered
    #     device
    #
    # * *Raises*:
    #   - +NoMatchingRequestError+:: if the challenge in the response doesn't match any of the
    #     provided ones
    #   - +ClientDataTypeError+:: if the response is of the wrong type
    #   - +AuthenticationFailedError+:: if the authentication failed
    #   - +UserNotPresentError+:: if the user wasn't present during the authentication
    #   - +CounterTooLowError+:: if there is a counter mismatch between the registered one and
    #     the one in the response
    #
    def authenticate!(challenge, response, registration_public_key, registration_counter)
      # TODO: check that it's the correct key_handle as well
      raise NoMatchingRequestError unless challenge == response.client_data.challenge

      raise ClientDataTypeError unless response.client_data.authentication?

      pem = U2F.public_key_pem(registration_public_key)

      raise AuthenticationFailedError unless response.verify(app_id, pem)

      raise UserNotPresentError unless response.user_present?

      unless response.counter > registration_counter
        raise CounterTooLowError unless response.counter.zero? && registration_counter.zero?
      end
    end

    ##
    # Generates a 32 byte long random U2F challenge
    #
    # * *Returns*:
    #   - Base64 urlsafe encoded challenge
    #
    def challenge
      ::U2F.urlsafe_encode64(SecureRandom.random_bytes(32))
    end

    ##
    # Generate data to be used when registering a U2F device
    #
    # * *Returns*:
    #   - An +Array+ of +RegisterRequest+ objects
    #
    def registration_requests
      # TODO: generate a request for each supported version
      [RegisterRequest.new(challenge)]
    end

    ##
    # Authenticate the response from the U2F device when registering
    #
    # * *Args*:
    #   - +challenges+:: +Array+ of challenge strings
    #   - +response+:: Response of the U2F device as a +RegisterResponse+ object
    #
    # * *Returns*:
    #   - A +Registration+ object
    #
    # * *Raises*:
    #   - +UnmatchedChallengeError+:: if the challenge in the response doesn't match any of
    #     the provided ones
    #   - +ClientDataTypeError+:: if the response is of the wrong type
    #   - +AttestationSignatureError+:: if the registration failed
    #
    def register!(challenges, response)
      challenges = [challenges] unless challenges.is_a? Array
      challenge = challenges.detect do |chg|
        chg == response.client_data.challenge
      end

      raise UnmatchedChallengeError unless challenge

      raise ClientDataTypeError unless response.client_data.registration?

      # Validate public key
      U2F.public_key_pem(response.public_key_raw)

      raise AttestationSignatureError unless response.verify(app_id)

      Registration.new(
        response.key_handle,
        response.public_key,
        response.certificate
      )
    end

    ##
    # Convert a binary public key to PEM format
    # * *Args*:
    #   - +key+:: Binary public key
    #
    # * *Returns*:
    #   - A base64 encoded public key +String+ in PEM format
    #
    # * *Raises*:
    #   - +PublicKeyDecodeError+:: if the +key+ argument is incorrect
    #
    def self.public_key_pem(key)
      raise PublicKeyDecodeError unless key.bytesize == 65 && key.byteslice(0) == "\x04"

      # http://tools.ietf.org/html/rfc5480
      der = OpenSSL::ASN1::Sequence(
        [
          OpenSSL::ASN1::Sequence(
            [
              OpenSSL::ASN1::ObjectId('1.2.840.10045.2.1'),  # id-ecPublicKey
              OpenSSL::ASN1::ObjectId('1.2.840.10045.3.1.7') # secp256r1
            ]
          ),
          OpenSSL::ASN1::BitString(key)
        ]
      ).to_der

      pem = "-----BEGIN PUBLIC KEY-----\r\n" +
            Base64.strict_encode64(der).scan(/.{1,64}/).join("\r\n") +
            "\r\n-----END PUBLIC KEY-----"
      pem
    end
  end

  ##
  # Variant of Base64::urlsafe_decode64 which adds padding if necessary
  #
  def self.urlsafe_decode64(string)
    string = case string.length % 4
             when 2 then string + '=='
             when 3 then string + '='
             else string
             end
    Base64.urlsafe_decode64(string)
  end

  ##
  # Variant of Base64::urlsafe_encode64 which removes padding
  #
  def self.urlsafe_encode64(string)
    Base64.urlsafe_encode64(string).delete('=')
  end
end
