├── .gitignore ├── .travis.yml ├── Gemfile ├── README.markdown ├── Rakefile ├── examples ├── http.rb ├── http2.rb ├── imap.rb ├── mechanize.rb └── smtp.rb ├── lib ├── ntlm.rb └── ntlm │ ├── http.rb │ ├── imap.rb │ ├── message.rb │ ├── smtp.rb │ ├── util.rb │ └── version.rb ├── ruby-ntlm.gemspec ├── test ├── auth_test.rb ├── function_test.rb └── test_helper.rb └── unused ├── extconf.rb ├── http_example.rb └── ntlm.c /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | - 2.2.7 5 | - 2.3.4 6 | - 2.4.1 7 | - jruby 8 | script: bundle exec rake test 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # vim: set ft=ruby: 2 | source 'https://rubygems.org' 3 | gemspec 4 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/macks/ruby-ntlm.svg?branch=master)](https://travis-ci.org/macks/ruby-ntlm) 2 | 3 | ruby-ntlm 4 | ========= 5 | 6 | ruby-ntlm is NTLM authentication client for Ruby. 7 | This library supports NTLM v1 only. 8 | 9 | NTLM authentication is used in Microsoft's server products, 10 | such as MS Exchange Server and IIS. 11 | 12 | 13 | Install 14 | ------- 15 | 16 | $ sudo gem install ruby-ntlm 17 | 18 | 19 | Usage 20 | ----- 21 | 22 | ### HTTP ### 23 | 24 | require 'ntlm/http' 25 | http = Net::HTTP.new('www.example.com') 26 | request = Net::HTTP::Get.new('/') 27 | request.ntlm_auth('User', 'Domain', 'Password') 28 | response = http.request(request) 29 | 30 | ### IMAP ### 31 | 32 | require 'ntlm/imap' 33 | imap = Net::IMAP.new('imap.example.com') 34 | imap.authenticate('NTLM', 'User', 'Domain', 'Password') 35 | 36 | ### SMTP ### 37 | 38 | require 'ntlm/smtp' 39 | smtp = Net::SMTP.new('smtp.example.com') 40 | smtp.start('localhost.localdomain', 'Domain\\User', 'Password', :ntlm) do |smtp| 41 | smtp.send_mail(mail_body, from_addr, to_addr) 42 | end 43 | 44 | 45 | Author 46 | ------ 47 | 48 | MATSUYAMA Kengo () 49 | 50 | 51 | License 52 | ------- 53 | 54 | MIT License. 55 | 56 | Copyright (c) 2010 MATSUYAMA Kengo 57 | 58 | 59 | References 60 | ---------- 61 | 62 | * [MS-NLMP][]: NT LAN Manager (NTLM) Authentication Protocol Specification 63 | [MS-NLMP]: http://msdn.microsoft.com/en-us/library/cc236621%28PROT.13%29.aspx 64 | * [Ruby/NTLM][]: Another NTLM implementation for Ruby 65 | [Ruby/NTLM]: https://rubygems.org/gems/rubyntlm 66 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # vim: set ft=ruby: 2 | 3 | require "bundler/gem_tasks" 4 | require 'rake/testtask' 5 | 6 | Rake::TestTask.new(:test) do |task| 7 | task.libs << 'lib:test' 8 | task.pattern = 'test/**/*_test.rb' 9 | task.verbose = true 10 | task.warning = true 11 | end 12 | -------------------------------------------------------------------------------- /examples/http.rb: -------------------------------------------------------------------------------- 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 20 | -------------------------------------------------------------------------------- /examples/http2.rb: -------------------------------------------------------------------------------- 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 10 | -------------------------------------------------------------------------------- /examples/imap.rb: -------------------------------------------------------------------------------- 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[]'] 11 | -------------------------------------------------------------------------------- /examples/mechanize.rb: -------------------------------------------------------------------------------- 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 10 | -------------------------------------------------------------------------------- /examples/smtp.rb: -------------------------------------------------------------------------------- 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 19 | -------------------------------------------------------------------------------- /lib/ntlm.rb: -------------------------------------------------------------------------------- 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 35 | -------------------------------------------------------------------------------- /lib/ntlm/http.rb: -------------------------------------------------------------------------------- 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 | @original_body = req.body 27 | req.body = nil 28 | start do 29 | req.delete('connection') 30 | return request(req, body, &block) 31 | end 32 | end 33 | 34 | # Negotiation 35 | req['authorization'] = 'NTLM ' + ::NTLM.negotiate.to_base64 36 | res = request_without_ntlm_auth(req, body) 37 | challenge = res['www-authenticate'][/NTLM (.*)/, 1].unpack('m').first rescue nil 38 | 39 | if challenge && res.code == '401' 40 | # Authentication 41 | user, domain, password = req.ntlm_auth_params 42 | req['authorization'] = 'NTLM ' + ::NTLM.authenticate(challenge, user, domain, password).to_base64 43 | req.body_stream.rewind if req.body_stream 44 | req.body = @original_body 45 | request_without_ntlm_auth(req, body, &block) # We must re-use the connection. 46 | else 47 | yield res if block_given? 48 | res 49 | end 50 | end 51 | 52 | end # HTTP 53 | 54 | end # Net 55 | -------------------------------------------------------------------------------- /lib/ntlm/imap.rb: -------------------------------------------------------------------------------- 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 38 | -------------------------------------------------------------------------------- /lib/ntlm/message.rb: -------------------------------------------------------------------------------- 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 | @domain = nil 86 | @workstation = nil 87 | @version = nil 88 | @target_info = nil 89 | @session_key = nil 90 | @mic = nil 91 | 92 | self.class::ATTRIBUTES.each do |key| 93 | instance_variable_set("@#{key}", args[key]) if args[key] 94 | end 95 | end 96 | 97 | def to_s 98 | serialize 99 | end 100 | 101 | def serialize_to_base64 102 | [serialize].pack('m').delete("\r\n") 103 | end 104 | 105 | alias to_base64 serialize_to_base64 106 | 107 | def has_flag?(symbol) 108 | (@flag & FLAGS[symbol]) != 0 109 | end 110 | 111 | def set(symbol) 112 | @flag |= FLAGS[symbol] 113 | end 114 | 115 | def clear(symbol) 116 | @flag &= ~FLAGS[symbol] 117 | end 118 | 119 | def unicode? 120 | has_flag?(:NEGOTIATE_UNICODE) 121 | end 122 | 123 | def inspect_flags 124 | flags = [] 125 | FLAGS.sort_by(&:last).each do |name, val| 126 | flags << name if (@flag & val).nonzero? 127 | end 128 | "[#{flags.join(', ')}]" 129 | end 130 | 131 | def inspect 132 | variables = (instance_variables.map(&:to_sym) - [:@offset, :@buffer, :@flag]).sort.map {|name| "#{name}=#{instance_variable_get(name).inspect}, " }.join 133 | "\#<#{self.class.name} #{variables}@flag=#{inspect_flags}>" 134 | end 135 | 136 | private 137 | 138 | def parse(string) 139 | @buffer = string 140 | signature, type = string.unpack('a8V') 141 | raise ParseError, 'Unknown signature' if signature != SSP_SIGNATURE 142 | raise ParseError, "Wrong type (expected #{self.class::TYPE}, but got #{type})" if type != self.class::TYPE 143 | end 144 | 145 | def append_payload(string, allocation_size = nil) 146 | size = string.size 147 | allocation_size ||= (size + 1) & ~1 148 | string = string.ljust(allocation_size, "\0") 149 | @buffer << string[0, allocation_size] 150 | result = [size, allocation_size, @offset].pack('vvV') 151 | @offset += allocation_size 152 | result 153 | end 154 | 155 | def fetch_payload(fields) 156 | size, _, offset = fields.unpack('vvV') 157 | return nil if size.zero? 158 | @buffer[offset, size] 159 | end 160 | 161 | def encode_version(array) 162 | array.pack('CCvx3C') # major, minor, build, ntlm revision 163 | end 164 | 165 | def decode_version(string) 166 | string.unpack('CCvx3C') # major, minor, build, ntlm revision 167 | end 168 | 169 | def decode_av_pair(string) 170 | result = [] 171 | string = string.dup 172 | while true 173 | id, length = string.slice!(0, 4).unpack('vv') 174 | value = string.slice!(0, length) 175 | 176 | case sym = AV_PAIR_NAMES[id] 177 | when :AV_EOL 178 | break 179 | when :AV_NB_COMPUTER_NAME, :AV_NB_DOMAIN_NAME, :AV_DNS_COMPUTER_NAME, :AV_DNS_DOMAIN_NAME, :AV_DNS_TREE_NAME, :AV_TARGET_NAME 180 | value = decode_utf16(value) 181 | when :AV_FLAGS 182 | value = data.unpack('V').first 183 | end 184 | 185 | result << [sym, value] 186 | end 187 | result 188 | end 189 | 190 | def encode_av_pair(av_pair) 191 | result = '' 192 | av_pair.each do |(id, value)| 193 | case id 194 | when :AV_NB_COMPUTER_NAME, :AV_NB_DOMAIN_NAME, :AV_DNS_COMPUTER_NAME, :AV_DNS_DOMAIN_NAME, :AV_DNS_TREE_NAME, :AV_TARGET_NAME 195 | value = encode_utf16(value) 196 | when :AV_FLAGS 197 | value = [data].pack('V') 198 | end 199 | result << [AV_PAIRS[id], value.size, value].pack('vva*') 200 | end 201 | 202 | result << [AV_EOL, 0].pack('vv') 203 | end 204 | 205 | 206 | # [MS-NLMP] 2.2.1.1 207 | class Negotiate < Message 208 | 209 | TYPE = 1 210 | ATTRIBUTES = [:domain, :workstation, :version] 211 | DEFAULT_FLAGS = [NEGOTIATE_UNICODE, NEGOTIATE_OEM, REQUEST_TARGET, NEGOTIATE_NTLM, NEGOTIATE_ALWAYS_SIGN, NEGOTIATE_EXTENDED_SECURITY].inject(:|) 212 | 213 | attr_accessor(*ATTRIBUTES) 214 | 215 | def parse(string) 216 | super 217 | @flag, domain, workstation, version = string.unpack('x12Va8a8a8') 218 | @domain = fetch_payload(domain) if has_flag?(:OEM_DOMAIN_SUPPLIED) 219 | @workstation = fetch_payload(workstation) if has_flag?(:OEM_WORKSTATION_SUPPLIED) 220 | @version = decode_version(version) if has_flag?(:NEGOTIATE_VERSION) 221 | self 222 | end 223 | 224 | def serialize 225 | @buffer = '' 226 | @offset = 40 # (8 + 4) + 4 + (8 * 3) 227 | 228 | if @domain 229 | set(:OEM_DOMAIN_SUPPLIED) 230 | domain = append_payload(@domain) 231 | end 232 | 233 | if @workstation 234 | set(:OEM_WORKSTATION_SUPPLIED) 235 | workstation = append_payload(@workstation) 236 | end 237 | 238 | if @version 239 | set(:NEGOTIATE_VERSION) 240 | version = encode_version(@version) 241 | end 242 | 243 | [SSP_SIGNATURE, TYPE, @flag, domain, workstation, version].pack('a8VVa8a8a8') + @buffer 244 | end 245 | 246 | end # Negotiate 247 | 248 | 249 | # [MS-NLMP] 2.2.1.2 250 | class Challenge < Message 251 | 252 | TYPE = 2 253 | ATTRIBUTES = [:target_name, :challenge, :target_info, :version] 254 | DEFAULT_FLAGS = 0 255 | 256 | attr_accessor(*ATTRIBUTES) 257 | 258 | def parse(string) 259 | super 260 | target_name, @flag, @challenge, target_info, version = string.unpack('x12a8Va8x8a8a8') 261 | @target_name = fetch_payload(target_name) if has_flag?(:REQUEST_TARGET) 262 | @target_info = fetch_payload(target_info) if has_flag?(:NEGOTIATE_TARGET_INFO) 263 | @version = decode_version(version) if has_flag?(:NEGOTIATE_VERSION) 264 | 265 | @target_name &&= decode_utf16(@target_name) if unicode? 266 | @target_info &&= decode_av_pair(@target_info) 267 | 268 | self 269 | end 270 | 271 | def serialize 272 | @buffer = '' 273 | @offset = 56 # (8 + 4) + 8 + 4 + (8 * 4) 274 | 275 | @challenge ||= OpenSSL::Random.random_bytes(8) 276 | 277 | if @target_name 278 | set(:REQUEST_TARGET) 279 | if unicode? 280 | target_name = append_payload(encode_utf16(@target_name)) 281 | else 282 | target_name = append_payload(@target_name) 283 | end 284 | end 285 | 286 | if @target_info 287 | set(:NEGOTIATE_TARGET_INFO) 288 | target_info = append_payload(encode_av_pair(@target_info)) 289 | end 290 | 291 | if @version 292 | set(:NEGOTIATE_VERSION) 293 | version = encode_version(@version) 294 | end 295 | 296 | [SSP_SIGNATURE, TYPE, target_name, @flag, @challenge, target_info, version].pack('a8Va8Va8x8a8a8') + @buffer 297 | end 298 | 299 | end # Challenge 300 | 301 | 302 | # [MS-NLMP] 2.2.1.3 303 | class Authenticate < Message 304 | 305 | TYPE = 3 306 | ATTRIBUTES = [:lm_response, :nt_response, :domain, :user, :workstation, :session_key, :version, :mic] 307 | DEFAULT_FLAGS = [NEGOTIATE_UNICODE, REQUEST_TARGET, NEGOTIATE_NTLM, NEGOTIATE_ALWAYS_SIGN, NEGOTIATE_EXTENDED_SECURITY].inject(:|) 308 | 309 | attr_accessor(*ATTRIBUTES) 310 | 311 | def parse(string) 312 | super 313 | lm_response, nt_response, domain, user, workstation, session_key, @flag, version, mic = \ 314 | string.unpack('x12a8a8a8a8a8a8Va8a16') 315 | 316 | @lm_response = fetch_payload(lm_response) 317 | @nt_response = fetch_payload(nt_response) 318 | @domain = fetch_payload(domain) 319 | @user = fetch_payload(user) 320 | @workstation = fetch_payload(workstation) 321 | @session_key = fetch_payload(session_key) if has_flag?(:NEGOTIATE_KEY_EXCH) 322 | @version = decode_version(version) if has_flag?(:NEGOTIATE_VERSION) 323 | @mic = mic 324 | 325 | if unicode? 326 | @domain = decode_utf16(@domain) 327 | @user = decode_utf16(@user) 328 | @workstation = decode_utf16(@workstation) 329 | end 330 | 331 | self 332 | end 333 | 334 | def serialize 335 | @buffer = '' 336 | @offset = 88 # (8 + 4) + (8 * 6) + 4 + 8 + 16 337 | 338 | lm_response = append_payload(@lm_response) 339 | nt_response = append_payload(@nt_response) 340 | 341 | if unicode? 342 | domain = append_payload(encode_utf16(@domain)) 343 | user = append_payload(encode_utf16(@user)) 344 | workstation = append_payload(encode_utf16(@workstation)) 345 | else 346 | domain = append_payload(@domain) 347 | user = append_payload(@user) 348 | workstation = append_payload(@workstation) 349 | end 350 | 351 | if @session_key 352 | set(:NEGOTIATE_KEY_EXCH) 353 | session_key = append_payload(@session_key) 354 | end 355 | 356 | if @version 357 | set(:NEGOTIATE_VERSION) 358 | version = encode_version(@version) 359 | end 360 | 361 | [SSP_SIGNATURE, TYPE, lm_response, nt_response, domain, user, workstation, session_key, @flag, version, @mic].pack('a8Va8a8a8a8a8a8Va8a16') + @buffer 362 | end 363 | 364 | end # Authenticate 365 | 366 | end # Message 367 | end # NTLM 368 | -------------------------------------------------------------------------------- /lib/ntlm/smtp.rb: -------------------------------------------------------------------------------- 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 31 | -------------------------------------------------------------------------------- /lib/ntlm/util.rb: -------------------------------------------------------------------------------- 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 | 62 | encrypted_text = cipher.update(plain_text) 63 | encrypted_text << cipher.final 64 | result << encrypted_text[0...8] 65 | end 66 | 67 | result 68 | end 69 | 70 | # [MS-NLMP] 3.3.1 71 | def lm_v1_hash(password) 72 | encrypt(LM_MAGIC_TEXT, password.upcase, 14) 73 | end 74 | 75 | # [MS-NLMP] 3.3.1 76 | def nt_v1_hash(password) 77 | OpenSSL::Digest::MD4.digest(encode_utf16(password)) 78 | end 79 | 80 | # [MS-NLMP] 3.3.1 81 | def ntlm_v1_response(challenge, password, options = {}) 82 | if options[:ntlm_v2_session] 83 | challenge = challenge.b if challenge.respond_to?(:b) 84 | client_challenge = options[:client_challenge] || OpenSSL::Random.random_bytes(8) 85 | client_challenge = client_challenge.b if client_challenge.respond_to?(:b) 86 | hash = OpenSSL::Digest::MD5.digest(challenge + client_challenge)[0, 8] 87 | nt_response = encrypt(hash, nt_v1_hash(password), 21) 88 | lm_response = client_challenge + ("\0" * 16) 89 | else 90 | nt_response = encrypt(challenge, nt_v1_hash(password), 21) 91 | lm_response = encrypt(challenge, lm_v1_hash(password), 21) 92 | end 93 | 94 | [nt_response, lm_response] 95 | end 96 | 97 | 98 | # [MS-NLMP] 3.3.2 99 | def nt_v2_hash(user, password, domain) 100 | user_domain = encode_utf16(user.upcase + domain) 101 | OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, nt_v1_hash(password), user_domain) 102 | end 103 | 104 | # [MS-NLMP] 3.3.2 105 | def ntlm_v2_response(*) 106 | raise NotImplemnetedError 107 | end 108 | 109 | end # Util 110 | end # NTLM 111 | -------------------------------------------------------------------------------- /lib/ntlm/version.rb: -------------------------------------------------------------------------------- 1 | # vim: set et sw=2 sts=2: 2 | 3 | module NTLM 4 | VERSION = '0.0.4' 5 | end 6 | -------------------------------------------------------------------------------- /ruby-ntlm.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'ntlm/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "ruby-ntlm" 8 | spec.version = NTLM::VERSION 9 | spec.authors = ["MATSUYAMA Kengo"] 10 | spec.email = ["macksx@gmail.com"] 11 | spec.summary = %q{NTLM implementation for Ruby.} 12 | spec.description = %q{NTLM implementation for Ruby.} 13 | spec.homepage = "http://github.com/macks/ruby-ntlm" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "bundler", "~> 1.5" 22 | spec.add_development_dependency "rake" 23 | spec.add_development_dependency "test-unit" 24 | end 25 | -------------------------------------------------------------------------------- /test/auth_test.rb: -------------------------------------------------------------------------------- 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 28 | -------------------------------------------------------------------------------- /test/function_test.rb: -------------------------------------------------------------------------------- 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 44 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 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 21 | -------------------------------------------------------------------------------- /unused/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | 3 | $CFLAGS = '-Wall -O2' 4 | $LDFLAGS = '-lntlm' 5 | 6 | if have_library('ntlm') 7 | create_makefile('ntlm') 8 | end 9 | 10 | -------------------------------------------------------------------------------- /unused/http_example.rb: -------------------------------------------------------------------------------- 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 21 | -------------------------------------------------------------------------------- /unused/ntlm.c: -------------------------------------------------------------------------------- 1 | /* vim: set et sw=2: 2 | * 3 | * NTLM for Ruby 4 | * by MATSUYAMA Kengo 5 | * 6 | */ 7 | 8 | #include 9 | #include 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 | } 41 | --------------------------------------------------------------------------------