rubyntlm 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,25 @@
1
+ = Ruby/NTLM -- NTLM Authentication Library for Ruby
2
+
3
+ Ruby/NTLM provides message creator and parser for the NTLM authentication.
4
+
5
+ Some features:
6
+ * Independent from non-standard Ruby libraries.
7
+ * Supports NTLM and NTLMv2 reponses.
8
+
9
+ == Simple Example
10
+
11
+ * Creating NTLM Type 1 message
12
+
13
+ t1 = NTLM::Message::Type1.new()
14
+
15
+ * Parsing NTLM Type 2 message from server
16
+
17
+ t2 = NTLM::Message.parse(message_from_server)
18
+
19
+ * Creating NTLM Type 3 message
20
+
21
+ t3 = t2.response({:user => 'user', :password => 'passwd'})
22
+
23
+ == Support
24
+
25
+ You can find Ruby/NTLM RubyForge page at http://rubyforge.org/projects/rubyntlm.
data/Rakefile ADDED
@@ -0,0 +1,66 @@
1
+ # Rakefile for rubyntlm -*- ruby -*-
2
+ # $Id: Rakefile,v 1.2 2006/10/05 01:36:52 koheik Exp $
3
+
4
+ require 'rake/rdoctask'
5
+ require 'rake/testtask'
6
+ require 'rake/packagetask'
7
+ require 'rake/gempackagetask'
8
+ require File.join(File.dirname(__FILE__), 'lib', 'net', 'ntlm')
9
+
10
+ PKG_NAME = 'rubyntlm'
11
+ PKG_VERSION = Net::NTLM::VERSION::STRING
12
+
13
+ task :default => [:test]
14
+
15
+ Rake::TestTask.new(:test) do |t|
16
+ t.test_files = FileList[ "test/*.rb" ]
17
+ t.warning = true
18
+ t.verbose = true
19
+ end
20
+
21
+ # Rake::PackageTask.new(PKG_NAME, PKG_VERSION) do |p|
22
+ # p.need_tar_gz = true
23
+ # p.package_dir = 'build'
24
+ # p.package_files.include("README", "Rakefile")
25
+ # p.package_files.include("lib/net/**/*.rb", "test/**/*.rb", "examples/**/*.rb")
26
+ # end
27
+
28
+ Rake::RDocTask.new do |rd|
29
+ rd.rdoc_dir = 'doc'
30
+ rd.title = 'Ruby/NTLM library'
31
+ rd.main = "README"
32
+ rd.rdoc_files.include("README", "lib/**/*.rb")
33
+ end
34
+
35
+ dist_dirs = ["lib", "test", "examples"]
36
+ spec = Gem::Specification.new do |s|
37
+ s.name = PKG_NAME
38
+ s.version = PKG_VERSION
39
+ s.summary = %q{Ruby/NTLM library.}
40
+ s.description = %q{Ruby/NTLM provides message creator and parser for the NTLM authentication.}
41
+ s.authors = ["Kohei Kajimoto"]
42
+ s.email = %q{koheik@gmail.com}
43
+ s.homepage = %q{http://rubyforge.org/projects/rubyntlm}
44
+ s.rubyforge_project = %q{rubyntlm}
45
+
46
+ s.files = ["Rakefile", "README"]
47
+ dist_dirs.each do |dir|
48
+ s.files = s.files + Dir.glob("#{dir}/**/*.rb")
49
+ end
50
+
51
+ s.has_rdoc = true
52
+ s.extra_rdoc_files = %w( README )
53
+ s.rdoc_options.concat ['--main', 'README']
54
+
55
+ s.autorequire = 'net/ntlm'
56
+ end
57
+
58
+ Rake::GemPackageTask.new(spec) do |p|
59
+ p.gem_spec = spec
60
+ p.need_tar = true
61
+ p.need_zip = true
62
+ p.package_dir = 'build'
63
+ end
64
+
65
+
66
+
data/examples/http.rb ADDED
@@ -0,0 +1,86 @@
1
+ # $Id: http.rb,v 1.2 2006/10/05 01:36:52 koheik Exp $
2
+ require 'socket'
3
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
4
+ require 'net/ntlm'
5
+
6
+ $user = nil
7
+ $passwd = nil
8
+
9
+ $host = "www"
10
+ $port = 80
11
+
12
+ def header(f, host)
13
+ f.print "GET / HTTP/1.1\r\n"
14
+ f.print "Host: #{host}\r\n"
15
+ f.print "Keep-Alive: 300\r\n"
16
+ f.print "Connection: keep-alive\r\n"
17
+ end
18
+
19
+ def main
20
+
21
+ s = TCPSocket.new($host, $port)
22
+
23
+ # client -> server
24
+ t1 = Net::NTLM::Message::Type1.new()
25
+ header(s, $host)
26
+ s.print "Authorization: NTLM " + t1.encode64 + "\r\n"
27
+ s.print "\r\n"
28
+
29
+ # server -> client
30
+ length = 0
31
+ while(line = s.gets)
32
+
33
+ if /^WWW-Authenticate: (NTLM|Negotiate) (.+)\r\n/ =~ line
34
+ msg = $2
35
+ end
36
+
37
+ if /^Content-Length: (\d+)\r\n/ =~ line
38
+ length = $1.to_i
39
+ end
40
+ if /^\r\n/ =~ line
41
+ if length > 0
42
+ cont = s.read(length)
43
+ end
44
+ break
45
+ end
46
+ end
47
+ t2 = Net::NTLM::Message.decode64(msg)
48
+
49
+ unless $user and $passwd
50
+ target = t2.target_name
51
+ target = Net::NTLM::decode_utf16le(target) if t2.has_flag?(:UNICODE)
52
+ puts "Target: #{target}"
53
+ print "User name: "
54
+ ($user = $stdin.readline).chomp!
55
+ print "Password: "
56
+ ($passwd = $stdin.readline).chomp!
57
+ end
58
+
59
+ # client -> server, again
60
+ t3 = t2.response({:user => $user, :password => $passwd}, {:ntlmv2 => true})
61
+ header(s, $host)
62
+ s.print "Authorization: NTLM " + t3.encode64 + "\r\n"
63
+ s.print "\r\n"
64
+
65
+ # server -> client
66
+ length = 0
67
+ while(line = s.gets)
68
+
69
+ if /^WWW-Authenticate: (NTLM|Negotiate) (.+)\r\n/ =~ line
70
+ msg = $2
71
+ end
72
+
73
+ if /^Content-Length: (\d+)\r\n/ =~ line
74
+ length = $1.to_i
75
+ end
76
+ if /^\r\n/ =~ line
77
+ if length > 0
78
+ p cont = s.read(length)
79
+ end
80
+ break
81
+ end
82
+ end
83
+ s.close
84
+ end
85
+
86
+ main
data/examples/imap.rb ADDED
@@ -0,0 +1,73 @@
1
+ # $Id: imap.rb,v 1.1 2006/10/05 01:36:52 koheik Exp $
2
+
3
+ require "net/imap"
4
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
5
+ require "net/ntlm"
6
+
7
+ Net::IMAP::debug = true
8
+
9
+ $host = "localhost"
10
+ $port = 143
11
+ $ssl = false
12
+ $user = nil
13
+ $passwd = nil
14
+
15
+ module Net
16
+ class IMAP
17
+ class NtlmAuthenticator
18
+ def process(data)
19
+ case @state
20
+ when 1
21
+ @state = 2
22
+ t1 = Net::NTLM::Message::Type1.new()
23
+ return t1.serialize
24
+ when 2
25
+ @state = 3
26
+ t2 = Net::NTLM::Message.parse(data)
27
+ t3 = t2.response({:user => @user, :password => @password}, {:ntlmv2 => (@ntlm_type == "ntlmv2")})
28
+ return t3.serialize
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def initialize(user, password, ntlm_type = "ntlmv2")
35
+ @user = user
36
+ @password = password
37
+ @ntlm_type = @ntlm_type
38
+ @state = 1
39
+ end
40
+ end
41
+ add_authenticator "NTLM", NtlmAuthenticator
42
+
43
+ class ResponseParser
44
+ def continue_req
45
+ match(T_PLUS)
46
+ if lookahead.symbol == T_CRLF # means empty message
47
+ return ContinuationRequest.new(ResponseText.new(nil, ""), @str)
48
+ end
49
+ match(T_SPACE)
50
+ return ContinuationRequest.new(resp_text, @str)
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ unless $user and $passwd
57
+ print "User name: "
58
+ ($user = $stdin.readline).chomp!
59
+ print "Password: "
60
+ ($passwd = $stdin.readline).chomp!
61
+ end
62
+
63
+ imap = Net::IMAP.new($host, $port, $ssl)
64
+ imap.authenticate("NTLM", $user, $passwd)
65
+ imap.examine("Inbox")
66
+ # imap.search(["RECENT"]).each do |message_id|
67
+ # envelope = imap.fetch(message_id, "ENVELOPE")[0].attr["ENVELOPE"]
68
+ # from = envelope.from.nil? ? "" : envelope.from[0].name
69
+ # subject = envelope.subject
70
+ # puts "#{message_id} #{from}: \t#{subject}"
71
+ # end
72
+ imap.logout
73
+ # imap.disconnect
data/examples/smtp.rb ADDED
@@ -0,0 +1,94 @@
1
+ # $Id: smtp.rb,v 1.2 2006/10/05 01:36:52 koheik Exp $
2
+ require 'socket'
3
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
4
+ require 'net/ntlm'
5
+
6
+ $user = nil
7
+ $passwd = nil
8
+
9
+ $host = "localhost"
10
+ $port = 25
11
+
12
+ $debug = true
13
+
14
+ def readline(f)
15
+ (l = f.gets).chomp!
16
+ puts "srv> " + l if $debug
17
+ l
18
+ end
19
+
20
+ def writeline(f, str)
21
+ puts "cli> " + str if $debug
22
+ f.print str + "\r\n"
23
+ end
24
+
25
+ def main
26
+ s = TCPSocket.new($host, $port)
27
+
28
+ # greetings
29
+ readline s
30
+ writeline s, "EHLO #{$host}"
31
+ while(line = readline(s))
32
+ login = true if /^250-AUTH=LOGIN/ =~ line
33
+ ntlm = true if /^250-AUTH.+NTLM.*/ =~ line
34
+ break if /^250 OK/ =~ line
35
+ end
36
+ unless ntlm and login
37
+ raise RuntimeError, "it looks like the server doesn't support NTLM Login"
38
+ end
39
+
40
+ # send Type1 Message
41
+ t1 = Net::NTLM::Message::Type1.new()
42
+ writeline s, "AUTH NTLM " + t1.encode64
43
+
44
+ # receive Type2 Message, i hope
45
+ line = readline s
46
+ unless /334 (.+)/ =~ line
47
+ raise RuntimeError, "i don't recognize this: #{line}"
48
+ end
49
+ t2 = Net::NTLM::Message.decode64($1)
50
+
51
+ unless $user and $passwd
52
+ target = t2.target_name
53
+ target = Net::NTLM::decode_utf16le(target) if t2.has_flag?(:UNICODE)
54
+ puts "Target: #{target}"
55
+ print "User name: "
56
+ ($user = $stdin.readline).chomp!
57
+ print "Password: "
58
+ ($passwd = $stdin.readline).chomp!
59
+ end
60
+
61
+ # send Type3 Message
62
+ t3 = t2.response({:user => $user, :password => $passwd}, {:ntlmv2 => true})
63
+ writeline s, t3.encode64
64
+
65
+ # and result is...
66
+ line = readline s
67
+
68
+ unless /^235(.+)Authentication successful./i =~ line
69
+ raise RuntimeError, "sorry, authentication failed."
70
+ end
71
+
72
+ # do real job here like...
73
+ # from = $user
74
+ # to = "billg"
75
+ # writeline s, "MAIL FROM: #{from}"
76
+ # readline s
77
+ # writeline s, "RCPT TO: #{to}"
78
+ # readline s
79
+ # writeline s, "DATA"
80
+ # readline s
81
+ # writeline s, "From: #{from}"
82
+ # writeline s, "To: #{to}"
83
+ # writeline s, "blab blab blab..."
84
+ # writeline s, "#{from}"
85
+ # writeline s, "."
86
+ # readline s
87
+
88
+ # say bye
89
+ writeline s, "QUIT"
90
+ s.close
91
+ end
92
+
93
+ main
94
+
data/lib/net/ntlm.rb ADDED
@@ -0,0 +1,774 @@
1
+ #
2
+ # = net/ntlm.rb
3
+ #
4
+ # An NTLM Authentication Library for Ruby
5
+ #
6
+ # This code is a derivative of "dbf2.rb" written by yrock
7
+ # and Minero Aoki. You can find original code here:
8
+ # http://jp.rubyist.net/magazine/?0013-CodeReview
9
+ # -------------------------------------------------------------
10
+ # Copyright (c) 2005,2006 yrock
11
+ #
12
+ # This program is free software.
13
+ # You can distribute/modify this program under the terms of the
14
+ # Ruby License.
15
+ #
16
+ # 2006-02-11 refactored by Minero Aoki
17
+ # -------------------------------------------------------------
18
+ #
19
+ # All protocol information used to write this code stems from
20
+ # "The NTLM Authentication Protocol" by Eric Glass. The author
21
+ # would thank to him for this tremendous work and making it
22
+ # available on the net.
23
+ # http://davenport.sourceforge.net/ntlm.html
24
+ # -------------------------------------------------------------
25
+ # Copyright (c) 2003 Eric Glass
26
+ #
27
+ # Permission to use, copy, modify, and distribute this document
28
+ # for any purpose and without any fee is hereby granted,
29
+ # provided that the above copyright notice and this list of
30
+ # conditions appear in all copies.
31
+ # -------------------------------------------------------------
32
+ #
33
+ # The author also looked Mozilla-Firefox-1.0.7 source code,
34
+ # namely, security/manager/ssl/src/nsNTLMAuthModule.cpp and
35
+ # Jonathan Bastien-Filiatrault's libntlm-ruby.
36
+ # "http://x2a.org/websvn/filedetails.php?
37
+ # repname=libntlm-ruby&path=%2Ftrunk%2Fntlm.rb&sc=1"
38
+ # The latter has a minor bug in its separate_keys function.
39
+ # The third key has to begin from the 14th character of the
40
+ # input string instead of 13th:)
41
+ #--
42
+ # $Id: ntlm.rb,v 1.1 2006/10/05 01:36:52 koheik Exp $
43
+ #++
44
+
45
+ require 'base64'
46
+ require 'openssl'
47
+ require 'openssl/digest'
48
+
49
+ module Net #:nodoc:
50
+ module NTLM
51
+
52
+ module VERSION #:nodoc:
53
+ MAJOR = 0
54
+ MINOR = 1
55
+ TINY = 1
56
+ STRING = [MAJOR, MINOR, TINY].join('.')
57
+ end
58
+
59
+ SSP_SIGN = "NTLMSSP\0"
60
+ BLOB_SIGN = 0x00000101
61
+ LM_MAGIC = "KGS!@\#$%"
62
+ TIME_OFFSET = 11644473600
63
+ MAX64 = 0xffffffffffffffff
64
+
65
+ FLAGS = {
66
+ :UNICODE => 0x00000001,
67
+ :OEM => 0x00000002,
68
+ :REQUEST_TARGET => 0x00000004,
69
+ # :UNKNOWN => 0x00000008,
70
+ :SIGN => 0x00000010,
71
+ :SEAL => 0x00000020,
72
+ # :UNKNOWN => 0x00000040,
73
+ :NETWARE => 0x00000100,
74
+ :NTLM => 0x00000200,
75
+ # :UNKNOWN => 0x00000400,
76
+ # :UNKNOWN => 0x00000800,
77
+ :DOMAIN_SUPPLIED => 0x00001000,
78
+ :WORKSTATION_SUPPLIED => 0x00002000,
79
+ :LOCAL_CALL => 0x00004000,
80
+ :ALWAYS_SIGN => 0x00008000,
81
+ :TARGET_TYPE_DOMAIN => 0x00010000,
82
+ :TARGET_INFO => 0x00800000,
83
+ :NTLM2_KEY => 0x00080000,
84
+ :KEY128 => 0x20000000,
85
+ :KEY56 => 0x80000000
86
+ }
87
+
88
+ FLAG_KEYS = FLAGS.keys.sort{|a, b| FLAGS[a] <=> FLAGS[b] }
89
+
90
+ DEFAULT_FLAGS = {
91
+ :TYPE1 => FLAGS[:UNICODE] | FLAGS[:OEM] | FLAGS[:REQUEST_TARGET] | FLAGS[:NTLM] | FLAGS[:ALWAYS_SIGN] | FLAGS[:NTLM2_KEY],
92
+ :TYPE2 => FLAGS[:UNICODE],
93
+ :TYPE3 => FLAGS[:UNICODE] | FLAGS[:REQUEST_TARGET] | FLAGS[:NTLM] | FLAGS[:ALWAYS_SIGN] | FLAGS[:NTLM2_KEY]
94
+ }
95
+
96
+ # module functions
97
+ class << self
98
+ def decode_utf16le(str)
99
+ Kconv.kconv(swap16(str), Kconv::ASCII, Kconv::UTF16)
100
+ end
101
+
102
+ def encode_utf16le(str)
103
+ swap16(Kconv.kconv(str, Kconv::UTF16, Kconv::ASCII))
104
+ end
105
+
106
+ def pack_int64le(val)
107
+ [val & 0x00000000ffffffff, val >> 32].pack("V2")
108
+ end
109
+
110
+ def swap16(str)
111
+ str.unpack("v*").pack("n*")
112
+ end
113
+
114
+ def split7(str)
115
+ s = str.dup
116
+ until s.empty?
117
+ (ret ||= []).push s.slice!(0, 7)
118
+ end
119
+ ret
120
+ end
121
+
122
+ def gen_keys(str)
123
+ split7(str).map{ |str7|
124
+ bits = split7(str7.unpack("B*")[0]).inject('')\
125
+ {|ret, tkn| ret += tkn + (tkn.gsub('1', '').size % 2).to_s }
126
+ [bits].pack("B*")
127
+ }
128
+ end
129
+
130
+ def apply_des(plain, keys)
131
+ dec = OpenSSL::Cipher::DES.new
132
+ keys.map {|k|
133
+ dec.key = k
134
+ dec.encrypt.update(plain)
135
+ }
136
+ end
137
+
138
+ def lm_hash(password)
139
+ keys = gen_keys password.upcase.ljust(14, "\0")
140
+ apply_des(LM_MAGIC, keys).join
141
+ end
142
+
143
+ def ntlm_hash(password, opt = {})
144
+ pwd = password.dup
145
+ unless opt[:unicode]
146
+ pwd = encode_utf16le(pwd)
147
+ end
148
+ OpenSSL::Digest::MD4.digest pwd
149
+ end
150
+
151
+ def ntlmv2_hash(user, password, target, opt={})
152
+ ntlmhash = ntlm_hash(password, opt)
153
+ userdomain = (user + target).upcase
154
+ unless opt[:unicode]
155
+ userdomain = encode_utf16le(userdomain)
156
+ end
157
+ OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, ntlmhash, userdomain)
158
+ end
159
+
160
+ # responses
161
+ def lm_response(arg)
162
+ begin
163
+ hash = arg[:lm_hash]
164
+ chal = arg[:challenge]
165
+ rescue
166
+ raise ArgumentError
167
+ end
168
+ chal = NTL::pack_int64le(chal) if chal.is_a?(Integer)
169
+ keys = gen_keys hash.ljust(21, "\0")
170
+ apply_des(chal, keys).join
171
+ end
172
+
173
+ def ntlm_response(arg)
174
+ hash = arg[:ntlm_hash]
175
+ chal = arg[:challenge]
176
+ chal = NTL::pack_int64le(chal) if chal.is_a?(Integer)
177
+ keys = gen_keys hash.ljust(21, "\0")
178
+ apply_des(chal, keys).join
179
+ end
180
+
181
+ def ntlmv2_response(arg, opt = {})
182
+ begin
183
+ key = arg[:ntlmv2_hash]
184
+ chal = arg[:challenge]
185
+ ti = arg[:target_info]
186
+ rescue
187
+ raise ArgumentError
188
+ end
189
+ chal = NTL::pack_int64le(chal) if chal.is_a?(Integer)
190
+
191
+ if opt[:client_challenge]
192
+ cc = opt[:client_challenge]
193
+ else
194
+ cc = rand(MAX64)
195
+ end
196
+ cc = NTLM::pack_int64le(cc) if cc.is_a?(Integer)
197
+
198
+ if opt[:timestamp]
199
+ ts = opt[:timestamp]
200
+ else
201
+ ts = Time.now.to_i
202
+ end
203
+ # epoch -> milsec from Jan 1, 1601
204
+ ts = 10000000 * (ts + TIME_OFFSET)
205
+
206
+ blob = Blob.new
207
+ blob.timestamp = ts
208
+ blob.challenge = cc
209
+ blob.target_info = ti
210
+
211
+ bb = blob.serialize
212
+ OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, key, chal + bb) + bb
213
+ end
214
+
215
+ def lmv2_response(arg, opt = {})
216
+ key = arg[:ntlmv2_hash]
217
+ chal = arg[:challenge]
218
+
219
+ chal = NTLM::pack_int64le(chal) if chal.is_a?(Integer)
220
+
221
+ if opt[:client_challenge]
222
+ cc = opt[:client_challenge]
223
+ else
224
+ cc = rand(MAX64)
225
+ end
226
+ cc = NTLM::pack_int64le(cc) if cc.is_a?(Integer)
227
+
228
+ OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, key, chal + cc) + cc
229
+ end
230
+
231
+ def ntlm2_session(arg, opt = {})
232
+ begin
233
+ passwd_hash = arg[:ntlm_hash]
234
+ chal = arg[:challenge]
235
+ rescue
236
+ raise ArgumentError
237
+ end
238
+
239
+ if opt[:client_challenge]
240
+ cc = opt[:client_challenge]
241
+ else
242
+ cc = rand(MAX64)
243
+ end
244
+ cc = NTLM::pack_int64le(cc) if cc.is_a?(Integer)
245
+
246
+ keys = gen_keys passwd_hash.ljust(21, "\0")
247
+ session_hash = OpenSSL::Digest::MD5.digest(chal + cc).slice(0, 8)
248
+ response = apply_des(session_hash, keys).join
249
+ [cc.ljust(24, "\0"), response]
250
+ end
251
+ end
252
+
253
+
254
+ # base classes for primitives
255
+ class Field
256
+ attr_accessor :active, :value
257
+
258
+ def initialize(opts)
259
+ @value = opts[:value]
260
+ @active = opts[:active].nil? ? true : opts[:active]
261
+ end
262
+
263
+ def size
264
+ @active ? @size : 0
265
+ end
266
+ end
267
+
268
+ class String < Field
269
+ def initialize(opts)
270
+ super(opts)
271
+ @size = opts[:size]
272
+ end
273
+
274
+ def parse(str, offset=0)
275
+ if @active and str.size >= offset + @size
276
+ @value = str[offset, @size]
277
+ @size
278
+ else
279
+ 0
280
+ end
281
+ end
282
+
283
+ def serialize
284
+ if @active
285
+ @value
286
+ else
287
+ ""
288
+ end
289
+ end
290
+
291
+ def value=(val)
292
+ @value = val
293
+ @size = @value.nil? ? 0 : @value.size
294
+ @active = (@size > 0)
295
+ end
296
+ end
297
+
298
+
299
+ class Int16LE < Field
300
+ def initialize(opt)
301
+ super(opt)
302
+ @size = 2
303
+ end
304
+ def parse(str, offset=0)
305
+ if @active and str.size >= offset + @size
306
+ @value = str[offset, @size].unpack("v")[0]
307
+ @size
308
+ else
309
+ 0
310
+ end
311
+ end
312
+
313
+ def serialize
314
+ [@value].pack("v")
315
+ end
316
+ end
317
+
318
+ class Int32LE < Field
319
+ def initialize(opt)
320
+ super(opt)
321
+ @size = 4
322
+ end
323
+
324
+ def parse(str, offset=0)
325
+ if @active and str.size >= offset + @size
326
+ @value = str.slice(offset, @size).unpack("V")[0]
327
+ @size
328
+ else
329
+ 0
330
+ end
331
+ end
332
+
333
+ def serialize
334
+ [@value].pack("V") if @active
335
+ end
336
+ end
337
+
338
+ class Int64LE < Field
339
+ def initialize(opt)
340
+ super(opt)
341
+ @size = 8
342
+ end
343
+
344
+ def parse(str, offset=0)
345
+ if @active and str.size >= offset + @size
346
+ d, u = str.slice(offset, @size).unpack("V2")
347
+ @value = (u * 0x100000000 + d)
348
+ @size
349
+ else
350
+ 0
351
+ end
352
+ end
353
+
354
+ def serialize
355
+ [@value & 0x00000000ffffffff, @value >> 32].pack("V2") if @active
356
+ end
357
+ end
358
+
359
+ # base class of data structure
360
+ class FieldSet
361
+ class << FieldSet
362
+ def define(&block)
363
+ c = Class.new(self)
364
+ def c.inherited(subclass)
365
+ proto = @proto
366
+ subclass.instance_eval {
367
+ @proto = proto
368
+ }
369
+ end
370
+ c.module_eval(&block)
371
+ c
372
+ end
373
+
374
+ def string(name, opts)
375
+ add_field(name, String, opts)
376
+ end
377
+
378
+ def int16LE(name, opts)
379
+ add_field(name, Int16LE, opts)
380
+ end
381
+
382
+ def int32LE(name, opts)
383
+ add_field(name, Int32LE, opts)
384
+ end
385
+
386
+ def int64LE(name, opts)
387
+ add_field(name, Int64LE, opts)
388
+ end
389
+
390
+ def security_buffer(name, opts)
391
+ add_field(name, SecurityBuffer, opts)
392
+ end
393
+
394
+ def prototypes
395
+ @proto
396
+ end
397
+
398
+ def names
399
+ @proto.map{|n, t, o| n}
400
+ end
401
+
402
+ def types
403
+ @proto.map{|n, t, o| t}
404
+ end
405
+
406
+ def opts
407
+ @proto.map{|n, t, o| o}
408
+ end
409
+
410
+ private
411
+
412
+ def add_field(name, type, opts)
413
+ (@proto ||= []).push [name, type, opts]
414
+ define_accessor name
415
+ end
416
+
417
+ def define_accessor(name)
418
+ module_eval(<<-End, __FILE__, __LINE__ + 1)
419
+ def #{name}
420
+ self['#{name}'].value
421
+ end
422
+
423
+ def #{name}=(val)
424
+ self['#{name}'].value = val
425
+ end
426
+ End
427
+ end
428
+ end
429
+
430
+ def initialize
431
+ @alist = self.class.prototypes.map{ |n, t, o| [n, t.new(o)] }
432
+ end
433
+
434
+ def serialize
435
+ @alist.map{|n, f| f.serialize }.join
436
+ end
437
+
438
+ def parse(str, offset=0)
439
+ @alist.inject(offset){|cur, a| cur += a[1].parse(str, cur)}
440
+ end
441
+
442
+ def size
443
+ @alist.inject(0){|sum, a| sum += a[1].size}
444
+ end
445
+
446
+ def [](name)
447
+ a = @alist.assoc(name.to_s.intern)
448
+ raise ArgumentError, "no such field: #{name}" unless a
449
+ a[1]
450
+ end
451
+
452
+ def []=(name, val)
453
+ a = @alist.assoc(name.to_s.intern)
454
+ raise ArgumentError, "no such field: #{name}" unless a
455
+ a[1] = val
456
+ end
457
+
458
+ def enable(name)
459
+ self[name].active = true
460
+ end
461
+
462
+ def disable(name)
463
+ self[name].active = false
464
+ end
465
+ end
466
+
467
+
468
+ Blob = FieldSet.define {
469
+ int32LE :blob_signature, {:value => BLOB_SIGN}
470
+ int32LE :reserved, {:value => 0}
471
+ int64LE :timestamp, {:value => 0}
472
+ string :challenge, {:value => "", :size => 8}
473
+ int32LE :unknown1, {:value => 0}
474
+ string :target_info, {:value => "", :size => 0}
475
+ int32LE :unknown2, {:value => 0}
476
+ }
477
+
478
+ SecurityBuffer = FieldSet.define {
479
+ int16LE :length, {:value => 0}
480
+ int16LE :allocated, {:value => 0}
481
+ int32LE :offset, {:value => 0}
482
+ }
483
+
484
+ class SecurityBuffer
485
+ attr_accessor :active
486
+ def initialize(opts)
487
+ super()
488
+ @value = opts[:value]
489
+ @active = opts[:active].nil? ? true : opts[:active]
490
+ @size = 8
491
+ end
492
+
493
+ def parse(str, offset=0)
494
+ if @active and str.size >= offset + @size
495
+ super(str, offset)
496
+ @value = str[self.offset, self.length]
497
+ @size
498
+ else
499
+ 0
500
+ end
501
+ end
502
+
503
+ def serialize
504
+ super if @active
505
+ end
506
+
507
+ def value
508
+ @value
509
+ end
510
+
511
+ def value=(val)
512
+ @value = val
513
+ self.length = self.allocated = val.size
514
+ end
515
+
516
+ def data_size
517
+ @active ? @value.size : 0
518
+ end
519
+ end
520
+
521
+ class Message < FieldSet
522
+ class << Message
523
+ def parse(str)
524
+ m = Type0.new
525
+ m.parse(str)
526
+ case m.type
527
+ when 1
528
+ t = Type1.parse(str)
529
+ when 2
530
+ t = Type2.parse(str)
531
+ when 3
532
+ t = Type3.parse(str)
533
+ else
534
+ raise ArgumentError, "unknown type: #{m.type}"
535
+ end
536
+ t
537
+ end
538
+
539
+ def decode64(str)
540
+ parse(Base64.decode64(str))
541
+ end
542
+ end
543
+
544
+ def has_flag?(flag)
545
+ (self[:flag].value & FLAGS[flag]) == FLAGS[flag]
546
+ end
547
+
548
+ def set_flag(flag)
549
+ self[:flag].value |= FLAGS[flag]
550
+ end
551
+
552
+ def dump_flags
553
+ FLAG_KEYS.each{ |k| print(k, "=", flag?(k), "\n") }
554
+ end
555
+
556
+ def serialize
557
+ deflag
558
+ super + security_buffers.map{|n, f| f.value}.join
559
+ end
560
+
561
+ def encode64
562
+ Base64.encode64(serialize).gsub(/\n/, '')
563
+ end
564
+
565
+ def decode64(str)
566
+ parse(Base64.decode64(str))
567
+ end
568
+
569
+ alias head_size size
570
+
571
+ def data_size
572
+ security_buffers.inject(0){|sum, a| sum += a[1].data_size}
573
+ end
574
+
575
+ def size
576
+ head_size + data_size
577
+ end
578
+
579
+
580
+ private
581
+
582
+ def security_buffers
583
+ @alist.find_all{|n, f| f.instance_of?(SecurityBuffer)}
584
+ end
585
+
586
+ def deflag
587
+ security_buffers.inject(head_size){|cur, a|
588
+ a[1].offset = cur
589
+ cur += a[1].data_size
590
+ }
591
+ end
592
+
593
+ def data_edge
594
+ security_buffers.map{ |n, f| f.active ? f.offset : size}.min
595
+ end
596
+
597
+ # sub class definitions
598
+
599
+ Type0 = Message.define {
600
+ string :sign, {:size => 8, :value => SSP_SIGN}
601
+ int32LE :type, {:value => 0}
602
+ }
603
+
604
+ Type1 = Message.define {
605
+ string :sign, {:size => 8, :value => SSP_SIGN}
606
+ int32LE :type, {:value => 1}
607
+ int32LE :flag, {:value => DEFAULT_FLAGS[:TYPE1] }
608
+ security_buffer :domain, {:value => "", :active => false}
609
+ security_buffer :workstation, {:value => "", :active => false}
610
+ string :padding, {:size => 0, :value => "", :active => false }
611
+ }
612
+
613
+ class Type1
614
+ class << Type1
615
+ def parse(str)
616
+ t = new
617
+ t.parse(str)
618
+ t
619
+ end
620
+ end
621
+
622
+ def parse(str)
623
+ super(str)
624
+ enable(:domain) if has_flag?(:DOMAIN_SUPPLIED)
625
+ enable(:workstation) if has_flag?(:WORKSTATION_SUPPLIED)
626
+ super(str)
627
+ if ( (len = data_edge - head_size) > 0)
628
+ self.padding = "\0" * len
629
+ super(str)
630
+ end
631
+ end
632
+ end
633
+
634
+ Type2 = Message.define{
635
+ string :sign, {:size => 8, :value => SSP_SIGN}
636
+ int32LE :type, {:value => 2}
637
+ security_buffer :target_name, {:size => 0, :value => ""}
638
+ int32LE :flag, {:value => DEFAULT_FLAGS[:TYPE2]}
639
+ int64LE :challenge, {:value => 0}
640
+ int64LE :context, {:value => 0, :active => false}
641
+ security_buffer :target_info, {:value => "", :active => false}
642
+ string :padding, {:size => 0, :value => "", :active => false }
643
+ }
644
+
645
+ class Type2
646
+ class << Type2
647
+ def parse(str)
648
+ t = new
649
+ t.parse(str)
650
+ t
651
+ end
652
+ end
653
+
654
+ def parse(str)
655
+ super(str)
656
+ if has_flag?(:TARGET_INFO)
657
+ enable(:context)
658
+ enable(:target_info)
659
+ super(str)
660
+ end
661
+ if ( (len = data_edge - head_size) > 0)
662
+ self.padding = "\0" * len
663
+ super(str)
664
+ end
665
+ end
666
+
667
+ def response(arg, opt = {})
668
+ usr = arg[:user]
669
+ pwd = arg[:password]
670
+ if usr.nil? or pwd.nil?
671
+ raise ArgumentError, "user and password have to be supplied"
672
+ end
673
+
674
+ if opt[:workstation]
675
+ ws = opt[:workstation]
676
+ else
677
+ ws = ""
678
+ end
679
+
680
+ if opt[:client_challenge]
681
+ cc = opt[:client_challenge]
682
+ else
683
+ cc = rand(MAX64)
684
+ end
685
+ cc = NTLM::pack_int64le(cc) if cc.is_a?(Integer)
686
+ opt[:client_challenge] = cc
687
+
688
+ if has_flag?(:OEM) and opt[:unicode]
689
+ usr = NTLM::decode_utf16le(usr)
690
+ pwd = NTLM::decode_utf16le(pwd)
691
+ ws = NTLM::decode_utf16le(ws)
692
+ opt[:unicode] = false
693
+ end
694
+
695
+ if has_flag?(:UNICODE) and !opt[:unicode]
696
+ usr = NTLM::encode_utf16le(usr)
697
+ pwd = NTLM::encode_utf16le(pwd)
698
+ ws = NTLM::encode_utf16le(ws)
699
+ opt[:unicode] = true
700
+ end
701
+
702
+ tgt = self.target_name
703
+ ti = self.target_info
704
+
705
+ chal = self[:challenge].serialize
706
+
707
+ if opt[:ntlmv2]
708
+ ar = {:ntlmv2_hash => NTLM::ntlmv2_hash(usr, pwd, tgt, opt), :challenge => chal, :target_info => ti}
709
+ lm_res = NTLM::lmv2_response(ar, opt)
710
+ ntlm_res = NTLM::ntlmv2_response(ar, opt)
711
+ elsif has_flag?(:NTLM2_KEY)
712
+ ar = {:ntlm_hash => NTLM::ntlm_hash(pwd, opt), :challenge => chal}
713
+ lm_res, ntlm_res = NTLM::ntlm2_session(ar, opt)
714
+ else
715
+ lm_res = NTLM::lm_response(pwd, chal)
716
+ ntlm_res = NTLM::ntlm_response(pwd, chal)
717
+ end
718
+
719
+ Type3.create({
720
+ :lm_response => lm_res,
721
+ :ntlm_response => ntlm_res,
722
+ :domain => tgt,
723
+ :user => usr,
724
+ :workstation => ws,
725
+ :flag => self.flag
726
+ })
727
+ end
728
+ end
729
+
730
+
731
+ Type3 = Message.define{
732
+ string :sign, {:size => 8, :value => SSP_SIGN}
733
+ int32LE :type, {:value => 3}
734
+ security_buffer :lm_response, {:value => ""}
735
+ security_buffer :ntlm_response, {:value => ""}
736
+ security_buffer :domain, {:value => ""}
737
+ security_buffer :user, {:value => ""}
738
+ security_buffer :workstation, {:value => ""}
739
+ security_buffer :session_key, {:value => "", :active => false }
740
+ int64LE :flag, {:value => 0, :active => false }
741
+ }
742
+
743
+ class Type3
744
+ class << Type3
745
+ def parse(str)
746
+ t = new
747
+ t.parse(str)
748
+ t
749
+ end
750
+
751
+ def create(arg, opt ={})
752
+ t = new
753
+ t.lm_response = arg[:lm_response]
754
+ t.ntlm_response = arg[:ntlm_response]
755
+ t.domain = arg[:domain]
756
+ t.user = arg[:user]
757
+ t.workstation = arg[:workstation]
758
+
759
+ if arg[:session_key]
760
+ t.enable(:session_key)
761
+ t.session_key = arg[session_key]
762
+ end
763
+ if arg[:flag]
764
+ t.enable(:session_key)
765
+ t.enable(:flag)
766
+ t.flag = arg[:flag]
767
+ end
768
+ t
769
+ end
770
+ end
771
+ end
772
+ end
773
+ end
774
+ end
@@ -0,0 +1,111 @@
1
+ # $Id$
2
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
3
+ require 'test/unit'
4
+ require 'net/ntlm'
5
+
6
+ class FunctionTest < Test::Unit::TestCase #:nodoc:
7
+ def setup
8
+ @passwd = "SecREt01"
9
+ @user = "user"
10
+ @domain = "domain"
11
+ @challenge = ["0123456789abcdef"].pack("H*")
12
+ @client_ch = ["ffffff0011223344"].pack("H*")
13
+ @timestamp = 1055844000
14
+ @trgt_info = [
15
+ "02000c0044004f004d00410049004e00" +
16
+ "01000c00530045005200560045005200" +
17
+ "0400140064006f006d00610069006e00" +
18
+ "2e0063006f006d000300220073006500" +
19
+ "72007600650072002e0064006f006d00" +
20
+ "610069006e002e0063006f006d000000" +
21
+ "0000"
22
+ ].pack("H*")
23
+ end
24
+
25
+ def test_lm_hash
26
+ ahash = ["ff3750bcc2b22412c2265b23734e0dac"].pack("H*")
27
+ assert_equal ahash, Net::NTLM::lm_hash(@passwd)
28
+ end
29
+
30
+ def test_ntlm_hash
31
+ ahash = ["cd06ca7c7e10c99b1d33b7485a2ed808"].pack("H*")
32
+ assert_equal ahash, Net::NTLM::ntlm_hash(@passwd)
33
+ end
34
+
35
+ def test_ntlmv2_hash
36
+ ahash = ["04b8e0ba74289cc540826bab1dee63ae"].pack("H*")
37
+ assert_equal ahash, Net::NTLM::ntlmv2_hash(@user, @passwd, @domain)
38
+ end
39
+
40
+ def test_lm_response
41
+ ares = ["c337cd5cbd44fc9782a667af6d427c6de67c20c2d3e77c56"].pack("H*")
42
+ assert_equal ares, Net::NTLM::lm_response(
43
+ {
44
+ :lm_hash => Net::NTLM::lm_hash(@passwd),
45
+ :challenge => @challenge
46
+ }
47
+ )
48
+ end
49
+
50
+ def test_ntlm_response
51
+ ares = ["25a98c1c31e81847466b29b2df4680f39958fb8c213a9cc6"].pack("H*")
52
+ ntlm_hash = Net::NTLM::ntlm_hash(@passwd)
53
+ assert_equal ares, Net::NTLM::ntlm_response(
54
+ {
55
+ :ntlm_hash => ntlm_hash,
56
+ :challenge => @challenge
57
+ }
58
+ )
59
+ end
60
+
61
+ def test_lmv2_response
62
+ ares = ["d6e6152ea25d03b7c6ba6629c2d6aaf0ffffff0011223344"].pack("H*")
63
+ assert_equal ares, Net::NTLM::lmv2_response(
64
+ {
65
+ :ntlmv2_hash => Net::NTLM::ntlmv2_hash(@user, @passwd, @domain),
66
+ :challenge => @challenge
67
+ },
68
+ { :client_challenge => @client_ch }
69
+ )
70
+ end
71
+
72
+ def test_ntlmv2_response
73
+ ares = [
74
+ "cbabbca713eb795d04c97abc01ee4983" +
75
+ "01010000000000000090d336b734c301" +
76
+ "ffffff00112233440000000002000c00" +
77
+ "44004f004d00410049004e0001000c00" +
78
+ "53004500520056004500520004001400" +
79
+ "64006f006d00610069006e002e006300" +
80
+ "6f006d00030022007300650072007600" +
81
+ "650072002e0064006f006d0061006900" +
82
+ "6e002e0063006f006d00000000000000" +
83
+ "0000"
84
+ ].pack("H*")
85
+ assert_equal ares, Net::NTLM::ntlmv2_response(
86
+ {
87
+ :ntlmv2_hash => Net::NTLM::ntlmv2_hash(@user, @passwd, @domain),
88
+ :challenge => @challenge,
89
+ :target_info => @trgt_info
90
+ },
91
+ {
92
+ :timestamp => @timestamp,
93
+ :client_challenge => @client_ch
94
+ }
95
+ )
96
+ end
97
+
98
+ def test_ntlm2_session
99
+ acha = ["ffffff001122334400000000000000000000000000000000"].pack("H*")
100
+ ares = ["10d550832d12b2ccb79d5ad1f4eed3df82aca4c3681dd455"].pack("H*")
101
+ session = Net::NTLM::ntlm2_session(
102
+ {
103
+ :ntlm_hash => Net::NTLM::ntlm_hash(@passwd),
104
+ :challenge => @challenge
105
+ },
106
+ { :client_challenge => @client_ch }
107
+ )
108
+ assert_equal acha, session[0]
109
+ assert_equal ares, session[1]
110
+ end
111
+ end
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.0
3
+ specification_version: 1
4
+ name: rubyntlm
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.1.1
7
+ date: 2006-10-04 00:00:00 -05:00
8
+ summary: Ruby/NTLM library.
9
+ require_paths:
10
+ - lib
11
+ email: koheik@gmail.com
12
+ homepage: http://rubyforge.org/projects/rubyntlm
13
+ rubyforge_project: rubyntlm
14
+ description: Ruby/NTLM provides message creator and parser for the NTLM authentication.
15
+ autorequire: net/ntlm
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Kohei Kajimoto
31
+ files:
32
+ - Rakefile
33
+ - README
34
+ - lib/net/ntlm.rb
35
+ - test/function_test.rb
36
+ - examples/http.rb
37
+ - examples/imap.rb
38
+ - examples/smtp.rb
39
+ test_files: []
40
+
41
+ rdoc_options:
42
+ - --main
43
+ - README
44
+ extra_rdoc_files:
45
+ - README
46
+ executables: []
47
+
48
+ extensions: []
49
+
50
+ requirements: []
51
+
52
+ dependencies: []
53
+