# frozen_string_literal: false # # = win32/sspi.rb # # Copyright (c) 2006-2007 Justin Bailey # # Written and maintained by Justin Bailey . # # This program is free software. You can re-distribute and/or # modify this program under the same terms of ruby itself --- # Ruby Distribution License or GNU General Public License. # require 'fiddle/import' # Implements bindings to Win32 SSPI functions, focused on authentication to a proxy server over HTTP. module Win32 module SSPI # Specifies how credential structure requested will be used. Only SECPKG_CRED_OUTBOUND is used # here. SECPKG_CRED_INBOUND = 0x00000001 SECPKG_CRED_OUTBOUND = 0x00000002 SECPKG_CRED_BOTH = 0x00000003 # Format of token. NETWORK format is used here. SECURITY_NATIVE_DREP = 0x00000010 SECURITY_NETWORK_DREP = 0x00000000 # InitializeSecurityContext Requirement flags ISC_REQ_REPLAY_DETECT = 0x00000004 ISC_REQ_SEQUENCE_DETECT = 0x00000008 ISC_REQ_CONFIDENTIALITY = 0x00000010 ISC_REQ_USE_SESSION_KEY = 0x00000020 ISC_REQ_PROMPT_FOR_CREDS = 0x00000040 ISC_REQ_CONNECTION = 0x00000800 # Win32 API Functions. Uses Win32API to bind methods to constants contained in class. module API extend Fiddle::Importer dlload "secur32.dll" [ # Can be called with AcquireCredentialsHandleA.call() "unsigned long AcquireCredentialsHandleA(void *, void *, unsigned long, void *, void *, void *, void *, void *, void *)", # Can be called with InitializeSecurityContextA.call() "unsigned long InitializeSecurityContextA(void *, void *, void *, unsigned long, unsigned long, unsigned long, void *, unsigned long, void *, void *, void *, void *)", # Can be called with DeleteSecurityContext.call() "unsigned long DeleteSecurityContext(void *)", # Can be called with FreeCredentialsHandle.call() "unsigned long FreeCredentialsHandle(void *)" ].each do |fn| cfunc = extern fn, :stdcall const_set cfunc.name.intern, cfunc end end # SecHandle struct class SecurityHandle def upper @struct.unpack("LL")[1] end def lower @struct.unpack("LL")[0] end def to_p @struct ||= "\0" * 8 end end # Some familiar aliases for the SecHandle structure CredHandle = CtxtHandle = SecurityHandle # TimeStamp struct class TimeStamp attr_reader :struct def to_p @struct ||= "\0" * 8 end end # Creates binary representations of a SecBufferDesc structure, # including the SecBuffer contained inside. class SecurityBuffer SECBUFFER_TOKEN = 2 # Security token TOKENBUFSIZE = 12288 SECBUFFER_VERSION = 0 def initialize(buffer = nil) @buffer = buffer || "\0" * TOKENBUFSIZE @bufferSize = @buffer.length @type = SECBUFFER_TOKEN end def bufferSize unpack @bufferSize end def bufferType unpack @type end def token unpack @buffer end def to_p # Assumption is that when to_p is called we are going to get a packed structure. Therefore, # set @unpacked back to nil so we know to unpack when accessors are next accessed. @unpacked = nil # Assignment of inner structure to variable is very important here. Without it, # will not be able to unpack changes to the structure. Alternative, nested unpacks, # does not work (i.e. @struct.unpack("LLP12")[2].unpack("LLP12") results in "no associated pointer") @sec_buffer ||= [@bufferSize, @type, @buffer].pack("LLP") @struct ||= [SECBUFFER_VERSION, 1, @sec_buffer].pack("LLP") end private # Unpacks the SecurityBufferDesc structure into member variables. We # only want to do this once per struct, so the struct is deleted # after unpacking. def unpack if ! @unpacked && @sec_buffer && @struct @bufferSize, @type = @sec_buffer.unpack("LL") @buffer = @sec_buffer.unpack("LLP#{@bufferSize}")[2] @struct = nil @sec_buffer = nil @unpacked = true end end end # SEC_WINNT_AUTH_IDENTITY structure class Identity SEC_WINNT_AUTH_IDENTITY_ANSI = 0x1 attr_accessor :user, :domain, :password def initialize(user = nil, domain = nil, password = nil) @user = user @domain = domain @password = password @flags = SEC_WINNT_AUTH_IDENTITY_ANSI end def to_p [@user, @user ? @user.length : 0, @domain, @domain ? @domain.length : 0, @password, @password ? @password.length : 0, @flags].pack("PLPLPLL") end end # Takes a return result from an SSPI function and interprets the value. class SSPIResult # Good results SEC_E_OK = 0x00000000 SEC_I_CONTINUE_NEEDED = 0x00090312 # These are generally returned by InitializeSecurityContext SEC_E_INSUFFICIENT_MEMORY = 0x80090300 SEC_E_INTERNAL_ERROR = 0x80090304 SEC_E_INVALID_HANDLE = 0x80090301 SEC_E_INVALID_TOKEN = 0x80090308 SEC_E_LOGON_DENIED = 0x8009030C SEC_E_NO_AUTHENTICATING_AUTHORITY = 0x80090311 SEC_E_NO_CREDENTIALS = 0x8009030E SEC_E_TARGET_UNKNOWN = 0x80090303 SEC_E_UNSUPPORTED_FUNCTION = 0x80090302 SEC_E_WRONG_PRINCIPAL = 0x80090322 # These are generally returned by AcquireCredentialsHandle SEC_E_NOT_OWNER = 0x80090306 SEC_E_SECPKG_NOT_FOUND = 0x80090305 SEC_E_UNKNOWN_CREDENTIALS = 0x8009030D @@map = {} constants.each { |v| @@map[self.const_get(v.to_s)] = v } attr_reader :value def initialize(value) # convert to unsigned long value = [value].pack("L").unpack("L").first raise "#{value.to_s(16)} is not a recognized result" unless @@map.has_key? value @value = value end def to_s @@map[@value].to_s end def ok? @value == SEC_I_CONTINUE_NEEDED || @value == SEC_E_OK end def ==(other) if other.is_a?(SSPIResult) @value == other.value elsif other.is_a?(Fixnum) @value == @@map[other] else false end end end # Handles "Negotiate" type authentication. Geared towards authenticating with a proxy server over HTTP class NegotiateAuth attr_accessor :credentials, :context, :contextAttributes, :user, :domain # Default request flags for SSPI functions REQUEST_FLAGS = ISC_REQ_CONFIDENTIALITY | ISC_REQ_REPLAY_DETECT | ISC_REQ_CONNECTION # NTLM tokens start with this header always. Encoding alone adds "==" and newline, so remove those B64_TOKEN_PREFIX = ["NTLMSSP"].pack("m").delete("=\n") # Given a connection and a request path, performs authentication as the current user and returns # the response from a GET request. The connnection should be a Net::HTTP object, and it should # have been constructed using the Net::HTTP.Proxy method, but anything that responds to "get" will work. # If a user and domain are given, will authenticate as the given user. # Returns the response received from the get method (usually Net::HTTPResponse) def NegotiateAuth.proxy_auth_get(http, path, user = nil, domain = nil) raise "http must respond to :get" unless http.respond_to?(:get) nego_auth = self.new user, domain resp = http.get path, { "Proxy-Authorization" => "Negotiate " + nego_auth.get_initial_token } if resp["Proxy-Authenticate"] resp = http.get path, { "Proxy-Authorization" => "Negotiate " + nego_auth.complete_authentication(resp["Proxy-Authenticate"].split(" ").last.strip) } end resp end # Creates a new instance ready for authentication as the given user in the given domain. # Defaults to current user and domain as defined by ENV["USERDOMAIN"] and ENV["USERNAME"] if # no arguments are supplied. def initialize(user = nil, domain = nil) if user.nil? && domain.nil? && ENV["USERNAME"].nil? && ENV["USERDOMAIN"].nil? raise "A username or domain must be supplied since they cannot be retrieved from the environment" end @user = user || ENV["USERNAME"] @domain = domain || ENV["USERDOMAIN"] end # Gets the initial Negotiate token. Returns it as a base64 encoded string suitable for use in HTTP. Can # be easily decoded, however. def get_initial_token raise "This object is no longer usable because its resources have been freed." if @cleaned_up get_credentials outputBuffer = SecurityBuffer.new @context = CtxtHandle.new @contextAttributes = "\0" * 4 result = SSPIResult.new(API::InitializeSecurityContextA.call(@credentials.to_p, nil, nil, REQUEST_FLAGS,0, SECURITY_NETWORK_DREP, nil, 0, @context.to_p, outputBuffer.to_p, @contextAttributes, TimeStamp.new.to_p)) if result.ok? then return encode_token(outputBuffer.token) else raise "Error: #{result.to_s}" end end # Takes a token and gets the next token in the Negotiate authentication chain. Token can be Base64 encoded or not. # The token can include the "Negotiate" header and it will be stripped. # Does not indicate if SEC_I_CONTINUE or SEC_E_OK was returned. # Token returned is Base64 encoded w/ all new lines removed. def complete_authentication(token) raise "This object is no longer usable because its resources have been freed." if @cleaned_up # Nil token OK, just set it to empty string token = "" if token.nil? if token.include? "Negotiate" # If the Negotiate prefix is passed in, assume we are seeing "Negotiate " and get the token. token = token.split(" ").last end if token.include? B64_TOKEN_PREFIX # indicates base64 encoded token token = token.strip.unpack("m")[0] end outputBuffer = SecurityBuffer.new result = SSPIResult.new(API::InitializeSecurityContext.call(@credentials.to_p, @context.to_p, nil, REQUEST_FLAGS, 0, SECURITY_NETWORK_DREP, SecurityBuffer.new(token).to_p, 0, @context.to_p, outputBuffer.to_p, @contextAttributes, TimeStamp.new.to_p)) if result.ok? then return encode_token(outputBuffer.token) else raise "Error: #{result.to_s}" end ensure # need to make sure we don't clean up if we've already cleaned up. clean_up unless @cleaned_up end private def clean_up # free structures allocated @cleaned_up = true API::FreeCredentialsHandle.call(@credentials.to_p) API::DeleteSecurityContext.call(@context.to_p) @context = nil @credentials = nil @contextAttributes = nil end # Gets credentials based on user, domain or both. If both are nil, an error occurs def get_credentials @credentials = CredHandle.new ts = TimeStamp.new @identity = Identity.new @user, @domain result = SSPIResult.new(API::AcquireCredentialsHandleA.call(nil, "Negotiate", SECPKG_CRED_OUTBOUND, nil, @identity.to_p, nil, nil, @credentials.to_p, ts.to_p)) raise "Error acquire credentials: #{result}" unless result.ok? end def encode_token(t) # encode64 will add newlines every 60 characters so we need to remove those. [t].pack("m").delete("\n") end end end end