stupidpupil/kerberos_authenticator

View on GitHub
lib/kerberos_authenticator/krb5/creds.rb

Summary

Maintainability
A
25 mins
Test Coverage
module KerberosAuthenticator
  module Krb5
    typedef :pointer, :krb5_creds

    attach_function :krb5_get_init_creds_password, [:krb5_context, :krb5_creds, :krb5_principal, :string, :pointer, :pointer, :int, :string, :pointer], :krb5_error_code
    attach_function :krb5_verify_init_creds, [:krb5_context, :krb5_creds, :krb5_principal, :pointer, :pointer, :pointer], :krb5_error_code

    attach_function :krb5_verify_init_creds_opt_init, [:pointer], :void
    attach_function :krb5_verify_init_creds_opt_set_ap_req_nofail, [:pointer, :bool], :void

    attach_function :krb5_free_cred_contents, [:krb5_context, :krb5_creds], :void
    attach_function :krb5_get_init_creds_opt_free, [:krb5_context, :pointer], :void

    attach_function :krb5_set_password, [:krb5_context, :krb5_creds, :string, :krb5_principal, :pointer, :pointer, :pointer], :krb5_error_code

    # Credentials, or tickets, provided by a KDC for a user.
    class Creds
      # @!attribute [r] ptr
      #   @return [FFI::Pointer] the pointer to the wrapped krb5_creds struct


      attr_reader :ptr

      # The size, in bytes, of the krb5_creds structure.
      # This differs between implementations and architectures.
      SIZE_OF_KRB5_CREDS = 480

      # Requests initial credentials for principal using password from a KDC.
      # @param principal [Principal] the user's Principal
      # @param password [String] the user's password
      # @param service [String] the service name used when requesting the credentials
      # @return [Creds]
      # @raise [Error] if a KDC for the principal can't be contacted
      # @raise [Error] if preauthentication fails
      # @see http://web.mit.edu/kerberos/krb5-1.14/doc/appdev/refs/api/krb5_get_init_creds_password.html krb5_get_init_creds_password
      # @see http://web.mit.edu/kerberos/krb5-1.14/doc/appdev/init_creds.html Initial credentials
      def self.initial_creds_for_principal_with_a_password(principal, password, service = nil)
        raise TypeError, 'expected Principal' unless principal.is_a? Principal

        ptr = FFI::MemoryPointer.new :char, SIZE_OF_KRB5_CREDS
        Krb5.get_init_creds_password(Context.context.ptr, ptr, principal.ptr, password.to_str, nil, nil, 0, service, nil)

        new(ptr)
      end

      # Initialize a new Keytab with a pointer to a krb5_keytab structure.
      # @param ptr [FFI::MemoryPointer]
      # @return [Keytab]
      def initialize(ptr)
        # HACK: AutoPointer won't accept a MemoryPointer, only a Pointer
        ptr.autorelease = false
        ptr = FFI::Pointer.new(ptr)

        ptr = FFI::AutoPointer.new ptr, self.class.method(:release)

        @ptr = ptr
      end

      # As of December 2018, Heimdal and the Apple fork don't cope with
      # a missing server principal correctly. We check to see if 
      #'get_host_princs_from_keytab' is implemented (which is what 
      # MIT Kerberos uses when server principal is NULL) and, if not, 
      # fall back on Heimdal's intended behaviour in #verify below.

      begin
        Krb5.attach_function :get_host_princs_from_keytab, [:pointer], :krb5_error_code
      rescue FFI::NotFoundError
        # Heimdal
        @@KRB5_DOES_NOT_SUPPORT_MISSING_SERVER_PRINCIPAL = true
        require 'socket' #For Socket.gethostname
      end


      # Calls #verify with nofail as true.
      # @return [TrueClass] always returns true if no error was raised
      # @see #verify
      def verify!(server_principal = nil, keytab = nil)
        verify(true, server_principal, keytab)
      end





      # Attempts to verify that these Creds were obtained from a KDC with knowledge of a key in keytab.
      # @param nofail [Boolean] whether to raise an Error if no keytab information is available
      # @param server_principal [Principal] the server principal to use choosing an entry in keytab
      # @param keytab [Keytab] the key table containing a key that the KDC should know
      # @raise [Error] if nofail is true and no keytab information is available
      # @raise [Error] if the KDC did not have knowledge of the key requested
      # @return [TrueClass] always returns true if no error was raised
      # @see http://web.mit.edu/kerberos/krb5-1.14/doc/appdev/refs/api/krb5_verify_init_creds.html krb5_verify_init_creds
      # @see http://web.mit.edu/kerberos/krb5-1.14/doc/appdev/refs/api/krb5_verify_init_creds_opt_set_ap_req_nofail.html krb5_verify_init_creds_opt_set_ap_req_nofail
      def verify(nofail = false, server_principal = nil, keytab = nil)
        verify_creds_opt = FFI::MemoryPointer.new :int, 2
        Krb5.verify_init_creds_opt_init(verify_creds_opt)
        Krb5.verify_init_creds_opt_set_ap_req_nofail(verify_creds_opt, nofail)

        server_princ_ptr = server_principal ? server_principal.ptr : nil


        if @@KRB5_DOES_NOT_SUPPORT_MISSING_SERVER_PRINCIPAL and !server_princ_ptr
          server_principal = Principal.new_with_name("host/#{Socket.gethostname}")
          server_princ_ptr = server_principal.ptr
        end

        keytab_ptr = keytab ? keytab.ptr : nil

        Krb5.verify_init_creds(Context.context.ptr, ptr, server_princ_ptr, keytab_ptr, nil, verify_creds_opt)

        true
      end

      # Sets a password for a principal using these Creds.
      # The Creds should be for the 'kadmin/changepw' service.
      # @param newpw [String] the new password
      # @param change_password_for [Principal] the Principal to change the password for
      # @raise [Error] if there is a problem making the password change request
      # @raise [Error] if server responds that the password change request failed
      # @return [TrueClass] always returns true if no error was raised
      # @see http://web.mit.edu/kerberos/krb5-1.14/doc/appdev/refs/api/krb5_set_password.html krb5_set_password
      def set_password(newpw, change_password_for = nil)
        change_password_for_ptr = change_password_for ? change_password_for.ptr : nil

        result_code = FFI::MemoryPointer.new :int
        result_code_string = Data.new
        result_string = Data.new

        Krb5.set_password(Context.context.ptr, ptr, newpw, change_password_for_ptr, result_code, result_code_string.pointer, result_string.pointer)

        result_code = result_code.read_uint
        result_string = result_string.read_string.force_encoding('UTF-8')
        raise SetPassError.new(result_code, result_string) if result_code > 0

        true
      end

      # Frees the contents of the Creds structure
      # @api private
      # @see http://web.mit.edu/kerberos/krb5-1.14/doc/appdev/refs/api/krb5_free_cred_contents.html krb5_free_cred_contents
      def self.release(pointer)
        Krb5.free_cred_contents(Context.context.ptr, pointer)
      end
    end
  end
end