ruby-ntlm 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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