1#
2# = win32/sspi.rb
3#
4# Copyright (c) 2006-2007 Justin Bailey
5#
6# Written and maintained by Justin Bailey <jgbailey@gmail.com>.
7#
8# This program is free software. You can re-distribute and/or
9# modify this program under the same terms of ruby itself ---
10# Ruby Distribution License or GNU General Public License.
11#
12
13require 'Win32API'
14
15# Implements bindings to Win32 SSPI functions, focused on authentication to a proxy server over HTTP.
16module Win32
17	module SSPI
18		# Specifies how credential structure requested will be used. Only SECPKG_CRED_OUTBOUND is used
19		# here.
20		SECPKG_CRED_INBOUND = 0x00000001
21		SECPKG_CRED_OUTBOUND = 0x00000002
22		SECPKG_CRED_BOTH = 0x00000003
23
24		# Format of token. NETWORK format is used here.
25		SECURITY_NATIVE_DREP = 0x00000010
26		SECURITY_NETWORK_DREP = 0x00000000
27
28		# InitializeSecurityContext Requirement flags
29		ISC_REQ_REPLAY_DETECT = 0x00000004
30		ISC_REQ_SEQUENCE_DETECT = 0x00000008
31		ISC_REQ_CONFIDENTIALITY = 0x00000010
32		ISC_REQ_USE_SESSION_KEY = 0x00000020
33		ISC_REQ_PROMPT_FOR_CREDS = 0x00000040
34		ISC_REQ_CONNECTION = 0x00000800
35
36		# Win32 API Functions. Uses Win32API to bind methods to constants contained in class.
37		module API
38			# Can be called with AcquireCredentialsHandle.call()
39			AcquireCredentialsHandle = Win32API.new("secur32", "AcquireCredentialsHandle", 'ppLpppppp', 'L')
40			# Can be called with InitializeSecurityContext.call()
41			InitializeSecurityContext = Win32API.new("secur32", "InitializeSecurityContext", 'pppLLLpLpppp', 'L')
42			# Can be called with DeleteSecurityContext.call()
43			DeleteSecurityContext = Win32API.new("secur32", "DeleteSecurityContext", 'P', 'L')
44			# Can be called with FreeCredentialsHandle.call()
45			FreeCredentialsHandle = Win32API.new("secur32", "FreeCredentialsHandle", 'P', 'L')
46		end
47
48		# SecHandle struct
49		class SecurityHandle
50			def upper
51				@struct.unpack("LL")[1]
52			end
53
54			def lower
55				@struct.unpack("LL")[0]
56			end
57
58			def to_p
59				@struct ||= "\0" * 8
60			end
61		end
62
63		# Some familiar aliases for the SecHandle structure
64		CredHandle = CtxtHandle = SecurityHandle
65
66		# TimeStamp struct
67		class TimeStamp
68			attr_reader :struct
69
70			def to_p
71				@struct ||= "\0" * 8
72			end
73		end
74
75		# Creates binary representaiton of a SecBufferDesc structure,
76		# including the SecBuffer contained inside.
77		class SecurityBuffer
78
79			SECBUFFER_TOKEN = 2   # Security token
80
81			TOKENBUFSIZE = 12288
82			SECBUFFER_VERSION = 0
83
84			def initialize(buffer = nil)
85				@buffer = buffer || "\0" * TOKENBUFSIZE
86				@bufferSize = @buffer.length
87				@type = SECBUFFER_TOKEN
88			end
89
90			def bufferSize
91				unpack
92				@bufferSize
93			end
94
95			def bufferType
96				unpack
97				@type
98			end
99
100			def token
101				unpack
102				@buffer
103			end
104
105			def to_p
106				# Assumption is that when to_p is called we are going to get a packed structure. Therefore,
107				# set @unpacked back to nil so we know to unpack when accessors are next accessed.
108				@unpacked = nil
109				# Assignment of inner structure to variable is very important here. Without it,
110				# will not be able to unpack changes to the structure. Alternative, nested unpacks,
111				# does not work (i.e. @struct.unpack("LLP12")[2].unpack("LLP12") results in "no associated pointer")
112				@sec_buffer ||= [@bufferSize, @type, @buffer].pack("LLP")
113				@struct ||= [SECBUFFER_VERSION, 1, @sec_buffer].pack("LLP")
114			end
115
116		private
117
118			# Unpacks the SecurityBufferDesc structure into member variables. We
119			# only want to do this once per struct, so the struct is deleted
120			# after unpacking.
121			def unpack
122				if ! @unpacked && @sec_buffer && @struct
123					@bufferSize, @type = @sec_buffer.unpack("LL")
124					@buffer = @sec_buffer.unpack("LLP#{@bufferSize}")[2]
125					@struct = nil
126					@sec_buffer = nil
127					@unpacked = true
128				end
129			end
130		end
131
132		# SEC_WINNT_AUTH_IDENTITY structure
133		class Identity
134			SEC_WINNT_AUTH_IDENTITY_ANSI = 0x1
135
136			attr_accessor :user, :domain, :password
137
138			def initialize(user = nil, domain = nil, password = nil)
139				@user = user
140				@domain = domain
141				@password = password
142				@flags = SEC_WINNT_AUTH_IDENTITY_ANSI
143			end
144
145			def to_p
146				[@user, @user ? @user.length : 0,
147				 @domain, @domain ? @domain.length : 0,
148				 @password, @password ? @password.length : 0,
149				 @flags].pack("PLPLPLL")
150			end
151		end
152
153		# Takes a return result from an SSPI function and interprets the value.
154		class SSPIResult
155			# Good results
156			SEC_E_OK = 0x00000000
157			SEC_I_CONTINUE_NEEDED = 0x00090312
158
159			# These are generally returned by InitializeSecurityContext
160			SEC_E_INSUFFICIENT_MEMORY = 0x80090300
161			SEC_E_INTERNAL_ERROR = 0x80090304
162			SEC_E_INVALID_HANDLE = 0x80090301
163			SEC_E_INVALID_TOKEN = 0x80090308
164			SEC_E_LOGON_DENIED = 0x8009030C
165			SEC_E_NO_AUTHENTICATING_AUTHORITY = 0x80090311
166			SEC_E_NO_CREDENTIALS = 0x8009030E
167			SEC_E_TARGET_UNKNOWN = 0x80090303
168			SEC_E_UNSUPPORTED_FUNCTION = 0x80090302
169			SEC_E_WRONG_PRINCIPAL = 0x80090322
170
171			# These are generally returned by AcquireCredentialsHandle
172			SEC_E_NOT_OWNER = 0x80090306
173			SEC_E_SECPKG_NOT_FOUND = 0x80090305
174			SEC_E_UNKNOWN_CREDENTIALS = 0x8009030D
175
176			@@map = {}
177			constants.each { |v| @@map[self.const_get(v.to_s)] = v }
178
179			attr_reader :value
180
181			def initialize(value)
182				# convert to unsigned long
183				value = [value].pack("L").unpack("L").first
184				raise "#{value.to_s(16)} is not a recognized result" unless @@map.has_key? value
185				@value = value
186			end
187
188			def to_s
189				@@map[@value].to_s
190			end
191
192			def ok?
193				@value == SEC_I_CONTINUE_NEEDED || @value == SEC_E_OK
194			end
195
196			def ==(other)
197				if other.is_a?(SSPIResult)
198					@value == other.value
199				elsif other.is_a?(Fixnum)
200					@value == @@map[other]
201				else
202					false
203				end
204			end
205		end
206
207		# Handles "Negotiate" type authentication. Geared towards authenticating with a proxy server over HTTP
208		class NegotiateAuth
209			attr_accessor :credentials, :context, :contextAttributes, :user, :domain
210
211			# Default request flags for SSPI functions
212			REQUEST_FLAGS = ISC_REQ_CONFIDENTIALITY | ISC_REQ_REPLAY_DETECT | ISC_REQ_CONNECTION
213
214			# NTLM tokens start with this header always. Encoding alone adds "==" and newline, so remove those
215      B64_TOKEN_PREFIX = ["NTLMSSP"].pack("m").delete("=\n")
216
217			# Given a connection and a request path, performs authentication as the current user and returns
218			# the response from a GET request. The connnection should be a Net::HTTP object, and it should
219			# have been constructed using the Net::HTTP.Proxy method, but anything that responds to "get" will work.
220			# If a user and domain are given, will authenticate as the given user.
221			# Returns the response received from the get method (usually Net::HTTPResponse)
222			def NegotiateAuth.proxy_auth_get(http, path, user = nil, domain = nil)
223				raise "http must respond to :get" unless http.respond_to?(:get)
224				nego_auth = self.new user, domain
225
226				resp = http.get path, { "Proxy-Authorization" => "Negotiate " + nego_auth.get_initial_token }
227				if resp["Proxy-Authenticate"]
228					resp = http.get path, { "Proxy-Authorization" => "Negotiate " + nego_auth.complete_authentication(resp["Proxy-Authenticate"].split(" ").last.strip) }
229				end
230
231				resp
232			end
233
234			# Creates a new instance ready for authentication as the given user in the given domain.
235			# Defaults to current user and domain as defined by ENV["USERDOMAIN"] and ENV["USERNAME"] if
236			# no arguments are supplied.
237			def initialize(user = nil, domain = nil)
238				if user.nil? && domain.nil? && ENV["USERNAME"].nil? && ENV["USERDOMAIN"].nil?
239					raise "A username or domain must be supplied since they cannot be retrieved from the environment"
240				end
241
242				@user = user || ENV["USERNAME"]
243				@domain = domain || ENV["USERDOMAIN"]
244			end
245
246			# Gets the initial Negotiate token. Returns it as a base64 encoded string suitable for use in HTTP. Can
247			# be easily decoded, however.
248			def get_initial_token
249				raise "This object is no longer usable because its resources have been freed." if @cleaned_up
250				get_credentials
251
252				outputBuffer = SecurityBuffer.new
253				@context = CtxtHandle.new
254				@contextAttributes = "\0" * 4
255
256				result = SSPIResult.new(API::InitializeSecurityContext.call(@credentials.to_p, nil, nil,
257					REQUEST_FLAGS,0, SECURITY_NETWORK_DREP, nil, 0, @context.to_p, outputBuffer.to_p, @contextAttributes, TimeStamp.new.to_p))
258
259				if result.ok? then
260					return encode_token(outputBuffer.token)
261				else
262					raise "Error: #{result.to_s}"
263				end
264			end
265
266			# Takes a token and gets the next token in the Negotiate authentication chain. Token can be Base64 encoded or not.
267			# The token can include the "Negotiate" header and it will be stripped.
268			# Does not indicate if SEC_I_CONTINUE or SEC_E_OK was returned.
269			# Token returned is Base64 encoded w/ all new lines removed.
270			def complete_authentication(token)
271				raise "This object is no longer usable because its resources have been freed." if @cleaned_up
272
273				# Nil token OK, just set it to empty string
274				token = "" if token.nil?
275
276				if token.include? "Negotiate"
277					# If the Negotiate prefix is passed in, assume we are seeing "Negotiate <token>" and get the token.
278					token = token.split(" ").last
279				end
280
281				if token.include? B64_TOKEN_PREFIX
282					# indicates base64 encoded token
283          token = token.strip.unpack("m")[0]
284				end
285
286				outputBuffer = SecurityBuffer.new
287				result = SSPIResult.new(API::InitializeSecurityContext.call(@credentials.to_p, @context.to_p, nil,
288					REQUEST_FLAGS, 0, SECURITY_NETWORK_DREP, SecurityBuffer.new(token).to_p, 0,
289					@context.to_p,
290					outputBuffer.to_p, @contextAttributes, TimeStamp.new.to_p))
291
292				if result.ok? then
293					return encode_token(outputBuffer.token)
294				else
295					raise "Error: #{result.to_s}"
296				end
297			ensure
298				# need to make sure we don't clean up if we've already cleaned up.
299				clean_up unless @cleaned_up
300			end
301
302		 private
303
304			def clean_up
305				# free structures allocated
306				@cleaned_up = true
307				API::FreeCredentialsHandle.call(@credentials.to_p)
308				API::DeleteSecurityContext.call(@context.to_p)
309				@context = nil
310				@credentials = nil
311				@contextAttributes = nil
312			end
313
314			# Gets credentials based on user, domain or both. If both are nil, an error occurs
315			def get_credentials
316				@credentials = CredHandle.new
317				ts = TimeStamp.new
318				@identity = Identity.new @user, @domain
319				result = SSPIResult.new(API::AcquireCredentialsHandle.call(nil, "Negotiate", SECPKG_CRED_OUTBOUND, nil, @identity.to_p,
320					nil, nil, @credentials.to_p, ts.to_p))
321				raise "Error acquire credentials: #{result}" unless result.ok?
322			end
323
324			def encode_token(t)
325				# encode64 will add newlines every 60 characters so we need to remove those.
326        [t].pack("m").delete("\n")
327			end
328		end
329	end
330end