ruby-ntlm 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown ADDED
@@ -0,0 +1,70 @@
1
+ ruby-ntlm
2
+ =========
3
+
4
+ ruby-ntlm is NTLM authentication client for Ruby.
5
+ This library supports NTLM v1 only.
6
+
7
+ NTLM authentication is used in Microsoft's server products,
8
+ such as MS Exchange Server and IIS.
9
+
10
+
11
+ Install
12
+ -------
13
+
14
+ $ sudo gem install ruby-ntlm
15
+
16
+
17
+ Usage
18
+ -----
19
+
20
+ ### HTTP ###
21
+
22
+ require 'ntlm/http'
23
+ http = Net::HTTP.new('www.example.com')
24
+ request = Net::HTTP::Get.new('/')
25
+ request.ntlm_auth('User', 'Domain', 'Password')
26
+ response = http.request(request)
27
+
28
+ ### HTTP (using Mechanize) ###
29
+
30
+ require 'ntlm/mechanize'
31
+ mech = Mechanize.new
32
+ mech.auth('Domain\\User', 'Password')
33
+ mech.get('http://www.example.com/index.html')
34
+
35
+ ### IMAP ###
36
+
37
+ require 'ntlm/imap'
38
+ imap = Net::IMAP.new('imap.example.com')
39
+ imap.authenticate('NTLM', 'User', 'Domain', 'Password')
40
+
41
+ ### SMTP ###
42
+
43
+ require 'ntlm/smtp'
44
+ smtp = Net::SMTP.new('smtp.example.com')
45
+ smtp.start('localhost.localdomain', 'Domain\\User', 'Password', :ntlm) do |smtp|
46
+ smtp.send_mail(mail_body, from_addr, to_addr)
47
+ end
48
+
49
+
50
+ Author
51
+ ------
52
+
53
+ MATSUYAMA Kengo (<macksx@gmail.com>)
54
+
55
+
56
+ License
57
+ -------
58
+
59
+ MIT License.
60
+
61
+ Copyright (c) 2010 MATSUYAMA Kengo
62
+
63
+
64
+ References
65
+ ----------
66
+
67
+ * [MS-NLMP][]: NT LAN Manager (NTLM) Authentication Protocol Specification
68
+ [MS-NLMP]: http://msdn.microsoft.com/en-us/library/cc236621%28PROT.13%29.aspx
69
+ * [Ruby/NTLM][]: Another NTLM implementation for Ruby
70
+ [Ruby/NTLM]: http://rubyforge.org/projects/rubyntlm/
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'jeweler'
4
+ require 'rake/testtask'
5
+
6
+ task :default => :test
7
+
8
+ Jeweler::Tasks.new do |gem|
9
+ gem.name = 'ruby-ntlm'
10
+ gem.summary = %Q{NTLM implementation for Ruby}
11
+ gem.description = %Q{NTLM implementation for Ruby.}
12
+ gem.email = 'macksx@gmail.com'
13
+ gem.homepage = 'http://github.com/macks/ruby-ntlm'
14
+ gem.authors = ['MATSUYAMA Kengo']
15
+ end
16
+
17
+ Rake::TestTask.new(:test) do |task|
18
+ task.libs << 'lib:test'
19
+ task.pattern = 'test/**/*_test.rb'
20
+ task.verbose = true
21
+ end
22
+
23
+ begin
24
+ require 'rcov/rcovtask'
25
+ Rcov::RcovTask.new do |task|
26
+ task.libs << 'lib:test'
27
+ task.pattern = 'test/**/*_test.rb'
28
+ task.verbose = true
29
+ end
30
+ rescue LoadError
31
+ task :rcov do
32
+ abort 'rcov is not available.'
33
+ end
34
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
data/examples/http.rb ADDED
@@ -0,0 +1,19 @@
1
+ require 'ntlm'
2
+ require 'net/http'
3
+
4
+ Net::HTTP.start('www.example.com') do |http|
5
+ request = Net::HTTP::Get.new('/')
6
+ request['authorization'] = 'NTLM ' + NTLM.negotiate.to_base64
7
+
8
+ response = http.request(request)
9
+
10
+ # The connection must be keep-alive!
11
+
12
+ challenge = response['www-authenticate'][/NTLM (.*)/, 1].unpack('m').first
13
+ request['authorization'] = 'NTLM ' + NTLM.authenticate(challenge, 'User', 'Domain', 'Password').to_base64
14
+
15
+ response = http.request(request)
16
+
17
+ p response
18
+ print response.body
19
+ end
data/examples/http2.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'ntlm/http'
2
+
3
+ http = Net::HTTP.new('www.example.com')
4
+ request = Net::HTTP::Get.new('/')
5
+ request.ntlm_auth('User', 'Domain', 'Password')
6
+ response = http.request(request)
7
+
8
+ p response
9
+ print response.body
data/examples/imap.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'ntlm/imap'
2
+
3
+ imap = Net::IMAP.new('imap.example.com')
4
+ abort 'NTLM authentication is not supported.' unless imap.capability.include?('AUTH=NTLM')
5
+ imap.authenticate('NTLM', 'User', 'Domain', 'Password')
6
+
7
+ imap.select('INBOX')
8
+ uids = imap.uid_search(['ALL'])
9
+ data = imap.uid_fetch(uids[0], 'BODY[]')
10
+ print data.first.attr['BODY[]']
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH << File.dirname(__FILE__) + '/lib'
2
+ require 'rubygems'
3
+ require 'ntlm/mechanize'
4
+
5
+ mech = Mechanize.new
6
+ mech.auth('Domain\\User', 'Password')
7
+ mech.get('http://www.example.com/index.html')
8
+
9
+ puts mech.page.body
data/examples/smtp.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'ntlm/smtp'
2
+
3
+ from_addr = 'from@example.com'
4
+ to_addr = 'to@example.com'
5
+
6
+ mail_body = <<-EOS
7
+ From: #{from_addr}
8
+ To: #{to_addr}
9
+ Subject: Example
10
+ Content-Type: text/plain
11
+
12
+ Hello world!
13
+ EOS
14
+
15
+ smtp = Net::SMTP.new('smtp.example.com')
16
+ smtp.start('localhost.localdomain', 'Domain\\User', 'Password', :ntlm) do |smtp|
17
+ smtp.send_mail(mail_body, from_addr, to_addr)
18
+ end
data/lib/ntlm.rb ADDED
@@ -0,0 +1,34 @@
1
+ # vim: set et sw=2 sts=2:
2
+
3
+ require 'ntlm/util'
4
+ require 'ntlm/message'
5
+
6
+ module NTLM
7
+
8
+ begin
9
+ Version = File.read(File.dirname(__FILE__) + '/../VERSION').strip
10
+ rescue
11
+ Version = 'unknown'
12
+ end
13
+
14
+ def self.negotiate(args = {})
15
+ Message::Negotiate.new(args)
16
+ end
17
+
18
+ def self.authenticate(challenge_message, user, domain, password, options = {})
19
+ challenge = Message::Challenge.parse(challenge_message)
20
+
21
+ opt = options.merge({
22
+ :ntlm_v2_session => challenge.has_flag?(:NEGOTIATE_EXTENDED_SECURITY),
23
+ })
24
+ nt_response, lm_response = Util.ntlm_v1_response(challenge.challenge, password, opt)
25
+
26
+ Message::Authenticate.new(
27
+ :user => user,
28
+ :domain => domain,
29
+ :lm_response => lm_response,
30
+ :nt_response => nt_response
31
+ )
32
+ end
33
+
34
+ end # NTLM
data/lib/ntlm/http.rb ADDED
@@ -0,0 +1,51 @@
1
+ require 'ntlm'
2
+ require 'net/http'
3
+
4
+ module Net
5
+
6
+ module HTTPHeader
7
+ attr_reader :ntlm_auth_params
8
+
9
+ def ntlm_auth(user, domain, password)
10
+ @ntlm_auth_params = [user, domain, password]
11
+ end
12
+ end
13
+
14
+ class HTTP
15
+
16
+ unless method_defined?(:request_without_ntlm_auth)
17
+ alias request_without_ntlm_auth request
18
+ end
19
+
20
+ def request(req, body = nil, &block)
21
+ unless req.ntlm_auth_params
22
+ return request_without_ntlm_auth(req, body, &block)
23
+ end
24
+
25
+ unless started?
26
+ start do
27
+ req.delete('connection')
28
+ return request(req, body, &block)
29
+ end
30
+ end
31
+
32
+ # Negotiation
33
+ req['authorization'] = 'NTLM ' + NTLM.negotiate.to_base64
34
+ res = request_without_ntlm_auth(req, body)
35
+ challenge = res['www-authenticate'][/NTLM (.*)/, 1].unpack('m').first rescue nil
36
+
37
+ if challenge && res.code == '401'
38
+ # Authentication
39
+ user, domain, password = req.ntlm_auth_params
40
+ req['authorization'] = 'NTLM ' + NTLM.authenticate(challenge, user, domain, password).to_base64
41
+ req.body_stream.rewind if req.body_stream
42
+ request_without_ntlm_auth(req, body, &block) # We must re-use the connection.
43
+ else
44
+ yield res if block_given?
45
+ res
46
+ end
47
+ end
48
+
49
+ end # HTTP
50
+
51
+ end # Net
data/lib/ntlm/imap.rb ADDED
@@ -0,0 +1,37 @@
1
+ require 'ntlm'
2
+ require 'net/imap'
3
+
4
+ module Net
5
+ class IMAP
6
+ class ResponseParser
7
+ def continue_req
8
+ match(T_PLUS)
9
+ if lookahead.symbol == T_CRLF
10
+ return ContinuationRequest.new(ResponseText.new(nil, ''), @str)
11
+ else
12
+ match(T_SPACE)
13
+ return ContinuationRequest.new(resp_text, @str)
14
+ end
15
+ end
16
+ end # ResponseParser
17
+
18
+ class NTLMAuthenticator
19
+ def initialize(user, domain, password)
20
+ @user, @domain, @password = user, domain, password
21
+ @state = 0
22
+ end
23
+
24
+ def process(data)
25
+ case (@state += 1)
26
+ when 1
27
+ NTLM.negotiate.to_s
28
+ when 2
29
+ NTLM.authenticate(data, @user, @domain, @password).to_s
30
+ end
31
+ end
32
+ end # NTLMAuthenticator
33
+
34
+ add_authenticator 'NTLM', NTLMAuthenticator
35
+
36
+ end # IMAP
37
+ end # Net
@@ -0,0 +1,42 @@
1
+ require 'mechanize'
2
+ require 'ntlm/http'
3
+
4
+ class Mechanize
5
+ class Chain
6
+ class AuthHeaders
7
+
8
+ unless method_defined?(:handle_without_ntlm)
9
+ alias handle_without_ntlm handle
10
+ end
11
+
12
+ def handle(ctx, params)
13
+ if @auth_hash[params[:uri].host] == :ntlm && @user && @password
14
+ if @user.index('\\')
15
+ domain, user = @user.split('\\', 2)
16
+ end
17
+ params[:request].ntlm_auth(user, domain, @password)
18
+ end
19
+ handle_without_ntlm(ctx, params)
20
+ end
21
+ end
22
+ end
23
+
24
+ unless private_method_defined?(:fetch_page_without_ntlm)
25
+ alias fetch_page_without_ntlm fetch_page
26
+ end
27
+
28
+ private
29
+
30
+ def fetch_page(params)
31
+ begin
32
+ fetch_page_without_ntlm(params)
33
+ rescue Mechanize::ResponseCodeError => e
34
+ if e.response_code == '401' && e.page.header['www-authenticate'] =~ /NTLM/ && @auth_hash[e.page.uri.host] != :ntlm
35
+ @auth_hash[e.page.uri.host] = :ntlm
36
+ fetch_page_without_ntlm(params)
37
+ else
38
+ raise
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,361 @@
1
+ # vim: set et sw=2 sts=2:
2
+
3
+ require 'ntlm/util'
4
+
5
+ module NTLM
6
+ class Message
7
+
8
+ include Util
9
+
10
+ SSP_SIGNATURE = "NTLMSSP\0"
11
+
12
+ # [MS-NLMP] 2.2.2.5
13
+ FLAGS = {
14
+ :NEGOTIATE_UNICODE => 0x00000001, # Unicode character set encoding
15
+ :NEGOTIATE_OEM => 0x00000002, # OEM character set encoding
16
+ :REQUEST_TARGET => 0x00000004, # TargetName is supplied in challenge message
17
+ :UNUSED10 => 0x00000008,
18
+ :NEGOTIATE_SIGN => 0x00000010, # Session key negotiation for message signatures
19
+ :NEGOTIATE_SEAL => 0x00000020, # Session key negotiation for message confidentiality
20
+ :NEGOTIATE_DATAGRAM => 0x00000040, # Connectionless authentication
21
+ :NEGOTIATE_LM_KEY => 0x00000080, # LAN Manager session key computation
22
+ :UNUSED9 => 0x00000100,
23
+ :NEGOTIATE_NTLM => 0x00000200, # NTLM v1 protocol
24
+ :UNUSED8 => 0x00000400,
25
+ :ANONYMOUS => 0x00000800, # Anonymous connection
26
+ :OEM_DOMAIN_SUPPLIED => 0x00001000, # Domain field is present
27
+ :OEM_WORKSTATION_SUPPLIED => 0x00002000, # Workstations field is present
28
+ :UNUSED7 => 0x00004000,
29
+ :NEGOTIATE_ALWAYS_SIGN => 0x00008000,
30
+ :TARGET_TYPE_DOMAIN => 0x00010000, # TargetName is domain name
31
+ :TARGET_TYPE_SERVER => 0x00020000, # TargetName is server name
32
+ :UNUSED6 => 0x00040000,
33
+ :NEGOTIATE_EXTENDED_SECURITY => 0x00080000, # NTLM v2 session security
34
+ :NEGOTIATE_IDENTIFY => 0x00100000, # Requests identify level token
35
+ :UNUSED5 => 0x00200000,
36
+ :REQUEST_NON_NT_SESSION_KEY => 0x00400000, # LM session key is used
37
+ :NEGOTIATE_TARGET_INFO => 0x00800000, # Requests TargetInfo
38
+ :UNUSED4 => 0x01000000,
39
+ :NEGOTIATE_VERSION => 0x02000000, # Version field is present
40
+ :UNUSED3 => 0x04000000,
41
+ :UNUSED2 => 0x08000000,
42
+ :UNUSED1 => 0x10000000,
43
+ :NEGOTIATE_128 => 0x20000000, # 128bit encryption
44
+ :NEGOTIATE_KEY_EXCH => 0x40000000, # Explicit key exchange
45
+ :NEGOTIATE_56 => 0x80000000, # 56bit encryption
46
+ }
47
+
48
+ # [MS-NLMP] 2.2.2.1
49
+ AV_PAIRS = {
50
+ :AV_EOL => 0,
51
+ :AV_NB_COMPUTER_NAME => 1,
52
+ :AV_NB_DOMAIN_NAME => 2,
53
+ :AV_DNS_COMPUTER_NAME => 3,
54
+ :AV_DNS_DOMAIN_NAME => 4,
55
+ :AV_DNS_TREE_NAME => 5,
56
+ :AV_FLAGS => 6,
57
+ :AV_TIMESTAMP => 7,
58
+ :AV_RESTRICTIONS => 8,
59
+ :AV_TARGET_NAME => 9,
60
+ :AV_CHANNEL_BINDINGS => 10,
61
+ }
62
+ AV_PAIR_NAMES = AV_PAIRS.invert
63
+
64
+ FLAGS.each do |name, val|
65
+ const_set(name, val)
66
+ end
67
+
68
+ AV_PAIRS.each do |name, val|
69
+ const_set(name, val)
70
+ end
71
+
72
+ class ParseError < StandardError; end
73
+
74
+ attr_accessor :flag
75
+
76
+
77
+ def self.parse(*args)
78
+ new.parse(*args)
79
+ end
80
+
81
+ def initialize(args = {})
82
+ @buffer = ''
83
+ @offset = 0
84
+ @flag = args[:flag] || self.class::DEFAULT_FLAGS
85
+
86
+ self.class::ATTRIBUTES.each do |key|
87
+ instance_variable_set("@#{key}", args[key]) if args[key]
88
+ end
89
+ end
90
+
91
+ def to_s
92
+ serialize
93
+ end
94
+
95
+ def serialize_to_base64
96
+ [serialize].pack('m').delete("\r\n")
97
+ end
98
+
99
+ alias to_base64 serialize_to_base64
100
+
101
+ def has_flag?(symbol)
102
+ (@flag & FLAGS[symbol]) != 0
103
+ end
104
+
105
+ def set(symbol)
106
+ @flag |= FLAGS[symbol]
107
+ end
108
+
109
+ def clear(symbol)
110
+ @flag &= ~FLAGS[symbol]
111
+ end
112
+
113
+ def unicode?
114
+ has_flag?(:NEGOTIATE_UNICODE)
115
+ end
116
+
117
+ def inspect_flags
118
+ flags = []
119
+ FLAGS.sort_by(&:last).each do |name, val|
120
+ flags << name if (@flag & val).nonzero?
121
+ end
122
+ "[#{flags.join(', ')}]"
123
+ end
124
+
125
+ def inspect
126
+ variables = (instance_variables.map(&:to_sym) - [:@offset, :@buffer, :@flag]).sort.map {|name| "#{name}=#{instance_variable_get(name).inspect}, " }.join
127
+ "\#<#{self.class.name} #{variables}@flag=#{inspect_flags}>"
128
+ end
129
+
130
+ private
131
+
132
+ def parse(string)
133
+ @buffer = string
134
+ signature, type = string.unpack('a8V')
135
+ raise ParseError, 'Unknown signature' if signature != SSP_SIGNATURE
136
+ raise ParseError, "Wrong type (expected #{self.class::TYPE}, but got #{type})" if type != self.class::TYPE
137
+ end
138
+
139
+ def append_payload(string, allocation_size = nil)
140
+ size = string.size
141
+ allocation_size ||= (size + 1) & ~1
142
+ string = string.ljust(allocation_size, "\0")
143
+ @buffer << string[0, allocation_size]
144
+ result = [size, allocation_size, @offset].pack('vvV')
145
+ @offset += allocation_size
146
+ result
147
+ end
148
+
149
+ def fetch_payload(fields)
150
+ size, allocated_size, offset = fields.unpack('vvV')
151
+ return nil if size.zero?
152
+ @buffer[offset, size]
153
+ end
154
+
155
+ def encode_version(array)
156
+ array.pack('CCvx3C') # major, minor, build, ntlm revision
157
+ end
158
+
159
+ def decode_version(string)
160
+ string.unpack('CCvx3C') # major, minor, build, ntlm revision
161
+ end
162
+
163
+ def decode_av_pair(string)
164
+ result = []
165
+ string = string.dup
166
+ while true
167
+ id, length = string.slice!(0, 4).unpack('vv')
168
+ value = string.slice!(0, length)
169
+
170
+ case sym = AV_PAIR_NAMES[id]
171
+ when :AV_EOL
172
+ break
173
+ when :AV_NB_COMPUTER_NAME, :AV_NB_DOMAIN_NAME, :AV_DNS_COMPUTER_NAME, :AV_DNS_DOMAIN_NAME, :AV_DNS_TREE_NAME, :AV_TARGET_NAME
174
+ value = decode_utf16(value)
175
+ when :AV_FLAGS
176
+ value = data.unpack('V').first
177
+ end
178
+
179
+ result << [sym, value]
180
+ end
181
+ result
182
+ end
183
+
184
+ def encode_av_pair(av_pair)
185
+ result = ''
186
+ av_pair.each do |(id, value)|
187
+ case id
188
+ when :AV_NB_COMPUTER_NAME, :AV_NB_DOMAIN_NAME, :AV_DNS_COMPUTER_NAME, :AV_DNS_DOMAIN_NAME, :AV_DNS_TREE_NAME, :AV_TARGET_NAME
189
+ value = encode_utf16(value)
190
+ when :AV_FLAGS
191
+ value = [data].pack('V')
192
+ end
193
+ result << [AV_PAIRS[id], value.size, value].pack('vva*')
194
+ end
195
+
196
+ result << [AV_EOL, 0].pack('vv')
197
+ end
198
+
199
+
200
+ # [MS-NLMP] 2.2.1.1
201
+ class Negotiate < Message
202
+
203
+ TYPE = 1
204
+ ATTRIBUTES = [:domain, :workstation, :version]
205
+ DEFAULT_FLAGS = [NEGOTIATE_UNICODE, NEGOTIATE_OEM, REQUEST_TARGET, NEGOTIATE_NTLM, NEGOTIATE_ALWAYS_SIGN, NEGOTIATE_EXTENDED_SECURITY].inject(:|)
206
+
207
+ attr_accessor *ATTRIBUTES
208
+
209
+ def parse(string)
210
+ super
211
+ @flag, domain, workstation, version = string.unpack('x12Va8a8a8')
212
+ @domain = fetch_payload(domain) if has_flag?(:OEM_DOMAIN_SUPPLIED)
213
+ @workstation = fetch_payload(workstation) if has_flag?(:OEM_WORKSTATION_SUPPLIED)
214
+ @version = decode_version(version) if has_flag?(:NEGOTIATE_VERSION)
215
+ self
216
+ end
217
+
218
+ def serialize
219
+ @buffer = ''
220
+ @offset = 40 # (8 + 4) + 4 + (8 * 3)
221
+
222
+ if @domain
223
+ set(:OEM_DOMAIN_SUPPLIED)
224
+ domain = append_payload(@domain)
225
+ end
226
+
227
+ if @workstation
228
+ set(:OEM_WORKSTATION_SUPPLIED)
229
+ workstation = append_payload(@workstation)
230
+ end
231
+
232
+ if @version
233
+ set(:NEGOTIATE_VERSION)
234
+ version = encode_version(@version)
235
+ end
236
+
237
+ [SSP_SIGNATURE, TYPE, @flag, domain, workstation, version].pack('a8VVa8a8a8') + @buffer
238
+ end
239
+
240
+ end # Negotiate
241
+
242
+
243
+ # [MS-NLMP] 2.2.1.2
244
+ class Challenge < Message
245
+
246
+ TYPE = 2
247
+ ATTRIBUTES = [:target_name, :challenge, :target_info, :version]
248
+ DEFAULT_FLAGS = 0
249
+
250
+ attr_accessor *ATTRIBUTES
251
+
252
+ def parse(string)
253
+ super
254
+ target_name, @flag, @challenge, target_info, version = string.unpack('x12a8Va8x8a8a8')
255
+ @target_name = fetch_payload(target_name) if has_flag?(:REQUEST_TARGET)
256
+ @target_info = fetch_payload(target_info) if has_flag?(:NEGOTIATE_TARGET_INFO)
257
+ @version = decode_version(version) if has_flag?(:NEGOTIATE_VERSION)
258
+
259
+ @target_name &&= decode_utf16(@target_name) if unicode?
260
+ @target_info &&= decode_av_pair(@target_info)
261
+
262
+ self
263
+ end
264
+
265
+ def serialize
266
+ @buffer = ''
267
+ @offset = 56 # (8 + 4) + 8 + 4 + (8 * 4)
268
+
269
+ @challenge ||= OpenSSL::Random.random_bytes(8)
270
+
271
+ if @target_name
272
+ set(:REQUEST_TARGET)
273
+ if unicode?
274
+ target_name = append_payload(encode_utf16(@target_name))
275
+ else
276
+ target_name = append_payload(@target_name)
277
+ end
278
+ end
279
+
280
+ if @target_info
281
+ set(:NEGOTIATE_TARGET_INFO)
282
+ target_info = append_payload(encode_av_pair(@target_info))
283
+ end
284
+
285
+ if @version
286
+ set(:NEGOTIATE_VERSION)
287
+ version = encode_version(@version)
288
+ end
289
+
290
+ [SSP_SIGNATURE, TYPE, target_name, @flag, @challenge, target_info, version].pack('a8Va8Va8x8a8a8') + @buffer
291
+ end
292
+
293
+ end # Challenge
294
+
295
+
296
+ # [MS-NLMP] 2.2.1.3
297
+ class Authenticate < Message
298
+
299
+ TYPE = 3
300
+ ATTRIBUTES = [:lm_response, :nt_response, :domain, :user, :workstation, :session_key, :version, :mic]
301
+ DEFAULT_FLAGS = [NEGOTIATE_UNICODE, REQUEST_TARGET, NEGOTIATE_NTLM, NEGOTIATE_ALWAYS_SIGN, NEGOTIATE_EXTENDED_SECURITY].inject(:|)
302
+
303
+ attr_accessor *ATTRIBUTES
304
+
305
+ def parse(string)
306
+ super
307
+ lm_response, nt_response, domain, user, workstation, session_key, @flag, version, mic = \
308
+ string.unpack('x12a8a8a8a8a8a8Va8a16')
309
+
310
+ @lm_response = fetch_payload(lm_response)
311
+ @nt_response = fetch_payload(nt_response)
312
+ @domain = fetch_payload(domain)
313
+ @user = fetch_payload(user)
314
+ @workstation = fetch_payload(workstation)
315
+ @session_key = fetch_payload(session_key) if has_flag?(:NEGOTIATE_KEY_EXCH)
316
+ @version = decode_version(version) if has_flag?(:NEGOTIATE_VERSION)
317
+ @mic = mic
318
+
319
+ if unicode?
320
+ @domain = decode_utf16(@domain)
321
+ @user = decode_utf16(@user)
322
+ @workstation = decode_utf16(@workstation)
323
+ end
324
+
325
+ self
326
+ end
327
+
328
+ def serialize
329
+ @buffer = ''
330
+ @offset = 88 # (8 + 4) + (8 * 6) + 4 + 8 + 16
331
+
332
+ lm_response = append_payload(@lm_response)
333
+ nt_response = append_payload(@nt_response)
334
+
335
+ if unicode?
336
+ domain = append_payload(encode_utf16(@domain))
337
+ user = append_payload(encode_utf16(@user))
338
+ workstation = append_payload(encode_utf16(@workstation))
339
+ else
340
+ domain = append_payload(@domain)
341
+ user = append_payload(@user)
342
+ workstation = append_payload(@workstation)
343
+ end
344
+
345
+ if @session_key
346
+ set(:NEGOTIATE_KEY_EXCH)
347
+ session_key = append_payload(@session_key)
348
+ end
349
+
350
+ if @version
351
+ set(:NEGOTIATE_VERSION)
352
+ version = encode_version(@version)
353
+ end
354
+
355
+ [SSP_SIGNATURE, TYPE, lm_response, nt_response, domain, user, workstation, session_key, @flag, version, @mic].pack('a8Va8a8a8a8a8a8Va8a16') + @buffer
356
+ end
357
+
358
+ end # Authenticate
359
+
360
+ end # Message
361
+ end # NTLM
data/lib/ntlm/smtp.rb ADDED
@@ -0,0 +1,30 @@
1
+ require 'ntlm'
2
+ require 'net/smtp'
3
+
4
+ module Net
5
+ class SMTP
6
+
7
+ def capable_ntlm_auth?
8
+ auth_capable?('NTLM')
9
+ end
10
+
11
+ def auth_ntlm(user, secret)
12
+ check_auth_args(user, secret)
13
+ if user.index('\\')
14
+ domain, user = user.split('\\', 2)
15
+ else
16
+ domain = ''
17
+ end
18
+
19
+ res = critical {
20
+ r = get_response("AUTH NTLM #{NTLM.negotiate.to_base64}")
21
+ check_auth_continue(r)
22
+ challenge = r.string.split(/ /, 2).last.unpack('m').first
23
+ get_response(NTLM.authenticate(challenge, user, domain, secret).to_base64)
24
+ }
25
+ check_auth_response(res)
26
+ res
27
+ end
28
+
29
+ end # SMTP
30
+ end # Net
data/lib/ntlm/util.rb ADDED
@@ -0,0 +1,105 @@
1
+ # vim: set et sw=2 sts=2:
2
+
3
+ require 'openssl'
4
+
5
+ module NTLM
6
+ module Util
7
+
8
+ LM_MAGIC_TEXT = 'KGS!@#$%'
9
+
10
+ module_function
11
+
12
+ if RUBY_VERSION >= '1.9'
13
+
14
+ def decode_utf16(str)
15
+ str.encode(Encoding::UTF_8, Encoding::UTF_16LE)
16
+ end
17
+
18
+ def encode_utf16(str)
19
+ str.to_s.encode(Encoding::UTF_16LE).force_encoding(Encoding::ASCII_8BIT)
20
+ end
21
+
22
+ else
23
+
24
+ require 'iconv'
25
+
26
+ def decode_utf16(str)
27
+ Iconv.conv('UTF-8', 'UTF-16LE', str)
28
+ end
29
+
30
+ def encode_utf16(str)
31
+ Iconv.conv('UTF-16LE', 'UTF-8', str)
32
+ end
33
+
34
+ end
35
+
36
+ def create_des_keys(string)
37
+ keys = []
38
+ string = string.dup
39
+ until (key = string.slice!(0, 7)).empty?
40
+ # key is 56 bits
41
+ key = key.unpack('B*').first
42
+ str = ''
43
+ until (bits = key.slice!(0, 7)).empty?
44
+ str << bits
45
+ str << (bits.count('1').even? ? '1' : '0') # parity
46
+ end
47
+ keys << [str].pack('B*')
48
+ end
49
+ keys
50
+ end
51
+
52
+ def encrypt(plain_text, key, key_length)
53
+ key = key.ljust(key_length, "\0")
54
+ keys = create_des_keys(key[0, key_length])
55
+
56
+ result = ''
57
+ cipher = OpenSSL::Cipher::DES.new
58
+ keys.each do |k|
59
+ cipher.encrypt
60
+ cipher.key = k
61
+ result << cipher.update(plain_text)
62
+ end
63
+
64
+ result
65
+ end
66
+
67
+ # [MS-NLMP] 3.3.1
68
+ def lm_v1_hash(password)
69
+ encrypt(LM_MAGIC_TEXT, password.upcase, 14)
70
+ end
71
+
72
+ # [MS-NLMP] 3.3.1
73
+ def nt_v1_hash(password)
74
+ OpenSSL::Digest::MD4.digest(encode_utf16(password))
75
+ end
76
+
77
+ # [MS-NLMP] 3.3.1
78
+ def ntlm_v1_response(challenge, password, options = {})
79
+ if options[:ntlm_v2_session]
80
+ client_challenge = options[:client_challenge] || OpenSSL::Random.random_bytes(8)
81
+ hash = OpenSSL::Digest::MD5.digest(challenge + client_challenge)[0, 8]
82
+ nt_response = encrypt(hash, nt_v1_hash(password), 21)
83
+ lm_response = client_challenge + ("\0" * 16)
84
+ else
85
+ nt_response = encrypt(challenge, nt_v1_hash(password), 21)
86
+ lm_response = encrypt(challenge, lm_v1_hash(password), 21)
87
+ end
88
+
89
+ [nt_response, lm_response]
90
+ end
91
+
92
+
93
+ # [MS-NLMP] 3.3.2
94
+ def nt_v2_hash(user, password, domain)
95
+ user_domain = encode_utf16(user.upcase + domain)
96
+ OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, nt_v1_hash(password), user_domain)
97
+ end
98
+
99
+ # [MS-NLMP] 3.3.2
100
+ def ntlm_v2_response(*)
101
+ raise NotImplemnetedError
102
+ end
103
+
104
+ end # Util
105
+ end # NTLM
data/test/auth_test.rb ADDED
@@ -0,0 +1,27 @@
1
+ # vim: set et sw=2 sts=2:
2
+
3
+ require File.dirname(__FILE__) + '/test_helper'
4
+
5
+ class AuthenticationTest < Test::Unit::TestCase
6
+
7
+ include NTLM::TestUtility
8
+ include NTLM::Util
9
+
10
+ def setup
11
+ @challenge = hex_to_bin("4e 54 4c 4d 53 53 50 00 02 00 00 00 0c 00 0c 00 38 00 00 00 05 82 01 00 11 11 11 11 11 11 11 11 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 44 00 6f 00 6d 00 61 00 69 00 6e 00")
12
+ end
13
+
14
+ def test_negotiate
15
+ assert_equal(hex_to_bin("4e 54 4c 4d 53 53 50 00 01 00 00 00 07 82 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"), NTLM.negotiate.to_s)
16
+ end
17
+
18
+ def test_authenticate
19
+ assert_equal(hex_to_bin("4e 54 4c 4d 53 53 50 00 03 00 00 00 18 00 18 00 58 00 00 00 18 00 18 00 70 00 00 00 0c 00 0c 00 88 00 00 00 08 00 08 00 94 00 00 00 00 00 00 00 9c 00 00 00 00 00 00 00 00 00 00 00 05 82 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 21 b0 c5 31 28 0e ed 8d 32 c3 1b ce b2 19 5a fd 58 2b b7 8e a0 d5 f2 78 8d 76 96 b7 58 49 16 14 2d 09 f0 a0 1f f2 35 10 be 2c ff 96 82 e0 e3 3b 44 00 6f 00 6d 00 61 00 69 00 6e 00 55 00 73 00 65 00 72 00"), NTLM.authenticate(@challenge, 'User', 'Domain', 'Password').to_s)
20
+
21
+ challenge = NTLM::Message::Challenge.parse(@challenge)
22
+ challenge.set(:NEGOTIATE_EXTENDED_SECURITY)
23
+
24
+ assert_equal(hex_to_bin("4e 54 4c 4d 53 53 50 00 03 00 00 00 18 00 18 00 58 00 00 00 18 00 18 00 70 00 00 00 0c 00 0c 00 88 00 00 00 08 00 08 00 94 00 00 00 00 00 00 00 9c 00 00 00 00 00 00 00 00 00 00 00 05 82 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 22 22 22 22 22 22 22 22 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 c2 1e db 62 54 34 d2 13 34 1a 04 3d f3 01 6d f3 01 c9 32 b4 ae 97 1e ac 44 00 6f 00 6d 00 61 00 69 00 6e 00 55 00 73 00 65 00 72 00"), NTLM.authenticate(challenge.to_s, 'User', 'Domain', 'Password', :client_challenge => "\x22" * 8).to_s)
25
+ end
26
+
27
+ end
@@ -0,0 +1,43 @@
1
+ # vim: set et sw=2 sts=2:
2
+
3
+ require File.dirname(__FILE__) + '/test_helper'
4
+
5
+ class FunctionTest < Test::Unit::TestCase
6
+ # Test pattern is borrowed from pyton-ntlm
7
+
8
+ include NTLM::TestUtility
9
+ include NTLM::Util
10
+
11
+ def setup
12
+ @server_challenge = hex_to_bin('01 23 45 67 89 ab cd ef')
13
+ @client_challenge = "\xaa" * 8
14
+ @time = "\0" * 8
15
+ @workstation = 'COMPUTER'
16
+ @server_name = 'Server'
17
+ @user = 'User'
18
+ @domain = 'Domain'
19
+ @password = 'Password'
20
+ @random_session_key = "\55" * 16
21
+ end
22
+
23
+ def test_lm_v1_hash
24
+ assert_equal(hex_to_bin("e5 2c ac 67 41 9a 9a 22 4a 3b 10 8f 3f a6 cb 6d"), lm_v1_hash(@password))
25
+ end
26
+
27
+ def test_nt_v1_hash
28
+ assert_equal(hex_to_bin("a4 f4 9c 40 65 10 bd ca b6 82 4e e7 c3 0f d8 52"), nt_v1_hash(@password))
29
+ end
30
+
31
+ def test_ntlm_v1_response
32
+ nt_response, lm_response = ntlm_v1_response(@server_challenge, @password)
33
+ assert_equal(hex_to_bin("67 c4 30 11 f3 02 98 a2 ad 35 ec e6 4f 16 33 1c 44 bd be d9 27 84 1f 94"), nt_response, 'nt_response')
34
+ assert_equal(hex_to_bin("98 de f7 b8 7f 88 aa 5d af e2 df 77 96 88 a1 72 de f1 1c 7d 5c cd ef 13"), lm_response, 'lm_response')
35
+ end
36
+
37
+ def test_ntlm_v1_response_with_ntlm_v2_session_security
38
+ nt_response, lm_response = ntlm_v1_response(@server_challenge, @password, :ntlm_v2_session => true, :client_challenge => @client_challenge)
39
+ assert_equal(hex_to_bin("75 37 f8 03 ae 36 71 28 ca 45 82 04 bd e7 ca f8 1e 97 ed 26 83 26 72 32"), nt_response, 'nt_response')
40
+ assert_equal(hex_to_bin("aa aa aa aa aa aa aa aa 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"), lm_response, 'lm_response')
41
+ end
42
+
43
+ end
@@ -0,0 +1,20 @@
1
+ # vim: set et sw=2 sts=2:
2
+
3
+ require 'test/unit'
4
+
5
+ $LOAD_PATH << File.dirname(__FILE__) + '/../lib'
6
+ require 'ntlm'
7
+
8
+ module NTLM
9
+ module TestUtility
10
+
11
+ def bin_to_hex(bin)
12
+ bin.unpack('H*').first.gsub(/..(?=.)/, '\0 ')
13
+ end
14
+
15
+ def hex_to_bin(hex)
16
+ [hex.delete(' ')].pack('H*')
17
+ end
18
+
19
+ end
20
+ end
data/unused/extconf.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'mkmf'
2
+
3
+ $CFLAGS = '-Wall -O2'
4
+ $LDFLAGS = '-lntlm'
5
+
6
+ if have_library('ntlm')
7
+ create_makefile('ntlm')
8
+ end
9
+
@@ -0,0 +1,20 @@
1
+ require 'ntlm.so'
2
+ require 'net/http'
3
+
4
+ Net::HTTP.start('host.localdomain') do |http|
5
+ request = Net::HTTP::Get.new('/')
6
+ request['authorization'] = 'NTLM ' + [NTLM.negotiate].pack('m').delete("\r\n")
7
+
8
+ response = http.request(request)
9
+
10
+ # Connection is keep-alive!
11
+
12
+ challenge = response['www-authenticate'][/NTLM (.*)/, 1].unpack('m').first
13
+ auth_response = NTLM.authenticate(challenge, 'User@Domain', 'Password')
14
+ request['authorization'] = 'NTLM ' + [auth_response].pack('m').delete("\r\n")
15
+
16
+ response = http.request(request)
17
+
18
+ p response
19
+ print response.body
20
+ end
data/unused/ntlm.c ADDED
@@ -0,0 +1,40 @@
1
+ /* vim: set et sw=2:
2
+ *
3
+ * NTLM for Ruby
4
+ * by MATSUYAMA Kengo
5
+ *
6
+ */
7
+
8
+ #include <ntlm.h>
9
+ #include <ruby.h>
10
+
11
+ static VALUE mNTLM;
12
+
13
+ static VALUE
14
+ ntlm_negotiate(VALUE obj)
15
+ {
16
+ tSmbNtlmAuthRequest request;
17
+ buildSmbNtlmAuthRequest(&request, "Workstation", "Domain");
18
+ return rb_str_new((const char *)&request, SmbLength(&request));
19
+ }
20
+
21
+ static VALUE
22
+ ntlm_authenticate(VALUE obj, VALUE challenge, VALUE user_at_domain, VALUE password)
23
+ {
24
+ tSmbNtlmAuthResponse response;
25
+
26
+ Check_Type(challenge, T_STRING);
27
+ Check_Type(user_at_domain, T_STRING);
28
+ Check_Type(password, T_STRING);
29
+
30
+ buildSmbNtlmAuthResponse((tSmbNtlmAuthChallenge *)RSTRING_PTR(challenge), &response, RSTRING_PTR(user_at_domain), RSTRING_PTR(password));
31
+
32
+ return rb_str_new((const char *)&response, SmbLength(&response));
33
+ }
34
+
35
+ void Init_ntlm()
36
+ {
37
+ mNTLM = rb_define_module("NTLM");
38
+ rb_define_module_function(mNTLM, "negotiate", ntlm_negotiate, 0);
39
+ rb_define_module_function(mNTLM, "authenticate", ntlm_authenticate, 3);
40
+ }
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-ntlm
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - MATSUYAMA Kengo
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-01-18 00:00:00 +09:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: NTLM implementation for Ruby.
23
+ email: macksx@gmail.com
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files:
29
+ - README.markdown
30
+ files:
31
+ - README.markdown
32
+ - Rakefile
33
+ - VERSION
34
+ - examples/http.rb
35
+ - examples/http2.rb
36
+ - examples/imap.rb
37
+ - examples/mechanize.rb
38
+ - examples/smtp.rb
39
+ - lib/ntlm.rb
40
+ - lib/ntlm/http.rb
41
+ - lib/ntlm/imap.rb
42
+ - lib/ntlm/mechanize.rb
43
+ - lib/ntlm/message.rb
44
+ - lib/ntlm/smtp.rb
45
+ - lib/ntlm/util.rb
46
+ - test/auth_test.rb
47
+ - test/function_test.rb
48
+ - test/test_helper.rb
49
+ - unused/extconf.rb
50
+ - unused/http_example.rb
51
+ - unused/ntlm.c
52
+ has_rdoc: true
53
+ homepage: http://github.com/macks/ruby-ntlm
54
+ licenses: []
55
+
56
+ post_install_message:
57
+ rdoc_options: []
58
+
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ hash: 3
67
+ segments:
68
+ - 0
69
+ version: "0"
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ hash: 3
76
+ segments:
77
+ - 0
78
+ version: "0"
79
+ requirements: []
80
+
81
+ rubyforge_project:
82
+ rubygems_version: 1.3.7
83
+ signing_key:
84
+ specification_version: 3
85
+ summary: NTLM implementation for Ruby
86
+ test_files:
87
+ - examples/http.rb
88
+ - examples/http2.rb
89
+ - examples/imap.rb
90
+ - examples/mechanize.rb
91
+ - examples/smtp.rb
92
+ - test/auth_test.rb
93
+ - test/function_test.rb
94
+ - test/test_helper.rb