lib/net/ntlm.rb in rubyntlm-0.6.1 vs lib/net/ntlm.rb in rubyntlm-0.6.2

- old
+ new

@@ -1,266 +1,266 @@ -# encoding: UTF-8 -# -# = net/ntlm.rb -# -# An NTLM Authentication Library for Ruby -# -# This code is a derivative of "dbf2.rb" written by yrock -# and Minero Aoki. You can find original code here: -# http://jp.rubyist.net/magazine/?0013-CodeReview -# ------------------------------------------------------------- -# Copyright (c) 2005,2006 yrock -# -# -# 2006-02-11 refactored by Minero Aoki -# ------------------------------------------------------------- -# -# All protocol information used to write this code stems from -# "The NTLM Authentication Protocol" by Eric Glass. The author -# would thank to him for this tremendous work and making it -# available on the net. -# http://davenport.sourceforge.net/ntlm.html -# ------------------------------------------------------------- -# Copyright (c) 2003 Eric Glass -# -# ------------------------------------------------------------- -# -# The author also looked Mozilla-Firefox-1.0.7 source code, -# namely, security/manager/ssl/src/nsNTLMAuthModule.cpp and -# Jonathan Bastien-Filiatrault's libntlm-ruby. -# "http://x2a.org/websvn/filedetails.php? -# repname=libntlm-ruby&path=%2Ftrunk%2Fntlm.rb&sc=1" -# The latter has a minor bug in its separate_keys function. -# The third key has to begin from the 14th character of the -# input string instead of 13th:) -#-- -# $Id: ntlm.rb,v 1.1 2006/10/05 01:36:52 koheik Exp $ -#++ - -require 'base64' -require 'openssl' -require 'openssl/digest' -require 'socket' - -# Load Order is important here -require 'net/ntlm/exceptions' -require 'net/ntlm/field' -require 'net/ntlm/int16_le' -require 'net/ntlm/int32_le' -require 'net/ntlm/int64_le' -require 'net/ntlm/string' - -require 'net/ntlm/field_set' -require 'net/ntlm/blob' -require 'net/ntlm/security_buffer' -require 'net/ntlm/message' -require 'net/ntlm/message/type0' -require 'net/ntlm/message/type1' -require 'net/ntlm/message/type2' -require 'net/ntlm/message/type3' - -require 'net/ntlm/encode_util' - -require 'net/ntlm/client' -require 'net/ntlm/channel_binding' -require 'net/ntlm/target_info' - -module Net - module NTLM - - LM_MAGIC = "KGS!@\#$%" - TIME_OFFSET = 11644473600 - MAX64 = 0xffffffffffffffff - - - class << self - - # Valid format for LAN Manager hex digest portion: 32 hexadecimal characters. - LAN_MANAGER_HEX_DIGEST_REGEXP = /[0-9a-f]{32}/i - # Valid format for NT LAN Manager hex digest portion: 32 hexadecimal characters. - NT_LAN_MANAGER_HEX_DIGEST_REGEXP = /[0-9a-f]{32}/i - # Valid format for an NTLM hash composed of `'<LAN Manager hex digest>:<NT LAN Manager hex digest>'`. - DATA_REGEXP = /\A#{LAN_MANAGER_HEX_DIGEST_REGEXP}:#{NT_LAN_MANAGER_HEX_DIGEST_REGEXP}\z/ - - # Takes a string and determines whether it is a valid NTLM Hash - # @param [String] the string to validate - # @return [Boolean] whether or not the string is a valid NTLM hash - def is_ntlm_hash?(data) - decoded_data = data.dup - decoded_data = EncodeUtil.decode_utf16le(decoded_data) - if DATA_REGEXP.match(decoded_data) - true - else - false - end - end - - # Conver the value to a 64-Bit Little Endian Int - # @param [String] val The string to convert - def pack_int64le(val) - [val & 0x00000000ffffffff, val >> 32].pack("V2") - end - - # Builds an array of strings that are 7 characters long - # @param [String] str The string to split - # @api private - def split7(str) - s = str.dup - until s.empty? - (ret ||= []).push s.slice!(0, 7) - end - ret - end - - # Not sure what this is doing - # @param [String] str String to generate keys for - # @api private - def gen_keys(str) - split7(str).map{ |str7| - bits = split7(str7.unpack("B*")[0]).inject('')\ - {|ret, tkn| ret += tkn + (tkn.gsub('1', '').size % 2).to_s } - [bits].pack("B*") - } - end - - def apply_des(plain, keys) - dec = OpenSSL::Cipher::Cipher.new("des-cbc") - dec.padding = 0 - keys.map {|k| - dec.key = k - dec.encrypt.update(plain) + dec.final - } - end - - # Generates a Lan Manager Hash - # @param [String] password The password to base the hash on - def lm_hash(password) - keys = gen_keys password.upcase.ljust(14, "\0") - apply_des(LM_MAGIC, keys).join - end - - # Generate a NTLM Hash - # @param [String] password The password to base the hash on - # @option opt :unicode (false) Unicode encode the password - def ntlm_hash(password, opt = {}) - pwd = password.dup - unless opt[:unicode] - pwd = EncodeUtil.encode_utf16le(pwd) - end - OpenSSL::Digest::MD4.digest pwd - end - - # Generate a NTLMv2 Hash - # @param [String] user The username - # @param [String] password The password - # @param [String] target The domain or workstation to authenticate to - # @option opt :unicode (false) Unicode encode the domain - def ntlmv2_hash(user, password, target, opt={}) - if is_ntlm_hash? password - decoded_password = EncodeUtil.decode_utf16le(password) - ntlmhash = [decoded_password.upcase[33,65]].pack('H32') - else - ntlmhash = ntlm_hash(password, opt) - end - userdomain = user.upcase + target - unless opt[:unicode] - userdomain = EncodeUtil.encode_utf16le(userdomain) - end - OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, ntlmhash, userdomain) - end - - def lm_response(arg) - begin - hash = arg[:lm_hash] - chal = arg[:challenge] - rescue - raise ArgumentError - end - chal = NTLM::pack_int64le(chal) if chal.is_a?(Integer) - keys = gen_keys hash.ljust(21, "\0") - apply_des(chal, keys).join - end - - def ntlm_response(arg) - hash = arg[:ntlm_hash] - chal = arg[:challenge] - chal = NTLM::pack_int64le(chal) if chal.is_a?(Integer) - keys = gen_keys hash.ljust(21, "\0") - apply_des(chal, keys).join - end - - def ntlmv2_response(arg, opt = {}) - begin - key = arg[:ntlmv2_hash] - chal = arg[:challenge] - ti = arg[:target_info] - rescue - raise ArgumentError - end - chal = NTLM::pack_int64le(chal) if chal.is_a?(Integer) - - if opt[:client_challenge] - cc = opt[:client_challenge] - else - cc = rand(MAX64) - end - cc = NTLM::pack_int64le(cc) if cc.is_a?(Integer) - - if opt[:timestamp] - ts = opt[:timestamp] - else - ts = Time.now.to_i - end - # epoch -> milsec from Jan 1, 1601 - ts = 10_000_000 * (ts + TIME_OFFSET) - - blob = Blob.new - blob.timestamp = ts - blob.challenge = cc - blob.target_info = ti - - bb = blob.serialize - - OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, key, chal + bb) + bb - end - - def lmv2_response(arg, opt = {}) - key = arg[:ntlmv2_hash] - chal = arg[:challenge] - - chal = NTLM::pack_int64le(chal) if chal.is_a?(Integer) - - if opt[:client_challenge] - cc = opt[:client_challenge] - else - cc = rand(MAX64) - end - cc = NTLM::pack_int64le(cc) if cc.is_a?(Integer) - - OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, key, chal + cc) + cc - end - - def ntlm2_session(arg, opt = {}) - begin - passwd_hash = arg[:ntlm_hash] - chal = arg[:challenge] - rescue - raise ArgumentError - end - chal = NTLM::pack_int64le(chal) if chal.is_a?(Integer) - - if opt[:client_challenge] - cc = opt[:client_challenge] - else - cc = rand(MAX64) - end - cc = NTLM::pack_int64le(cc) if cc.is_a?(Integer) - - keys = gen_keys(passwd_hash.ljust(21, "\0")) - session_hash = OpenSSL::Digest::MD5.digest(chal + cc).slice(0, 8) - response = apply_des(session_hash, keys).join - [cc.ljust(24, "\0"), response] - end - end - - end -end +# encoding: UTF-8 +# +# = net/ntlm.rb +# +# An NTLM Authentication Library for Ruby +# +# This code is a derivative of "dbf2.rb" written by yrock +# and Minero Aoki. You can find original code here: +# http://jp.rubyist.net/magazine/?0013-CodeReview +# ------------------------------------------------------------- +# Copyright (c) 2005,2006 yrock +# +# +# 2006-02-11 refactored by Minero Aoki +# ------------------------------------------------------------- +# +# All protocol information used to write this code stems from +# "The NTLM Authentication Protocol" by Eric Glass. The author +# would thank to him for this tremendous work and making it +# available on the net. +# http://davenport.sourceforge.net/ntlm.html +# ------------------------------------------------------------- +# Copyright (c) 2003 Eric Glass +# +# ------------------------------------------------------------- +# +# The author also looked Mozilla-Firefox-1.0.7 source code, +# namely, security/manager/ssl/src/nsNTLMAuthModule.cpp and +# Jonathan Bastien-Filiatrault's libntlm-ruby. +# "http://x2a.org/websvn/filedetails.php? +# repname=libntlm-ruby&path=%2Ftrunk%2Fntlm.rb&sc=1" +# The latter has a minor bug in its separate_keys function. +# The third key has to begin from the 14th character of the +# input string instead of 13th:) +#-- +# $Id: ntlm.rb,v 1.1 2006/10/05 01:36:52 koheik Exp $ +#++ + +require 'base64' +require 'openssl' +require 'openssl/digest' +require 'socket' + +# Load Order is important here +require 'net/ntlm/exceptions' +require 'net/ntlm/field' +require 'net/ntlm/int16_le' +require 'net/ntlm/int32_le' +require 'net/ntlm/int64_le' +require 'net/ntlm/string' + +require 'net/ntlm/field_set' +require 'net/ntlm/blob' +require 'net/ntlm/security_buffer' +require 'net/ntlm/message' +require 'net/ntlm/message/type0' +require 'net/ntlm/message/type1' +require 'net/ntlm/message/type2' +require 'net/ntlm/message/type3' + +require 'net/ntlm/encode_util' + +require 'net/ntlm/client' +require 'net/ntlm/channel_binding' +require 'net/ntlm/target_info' + +module Net + module NTLM + + LM_MAGIC = "KGS!@\#$%" + TIME_OFFSET = 11644473600 + MAX64 = 0xffffffffffffffff + + + class << self + + # Valid format for LAN Manager hex digest portion: 32 hexadecimal characters. + LAN_MANAGER_HEX_DIGEST_REGEXP = /[0-9a-f]{32}/i + # Valid format for NT LAN Manager hex digest portion: 32 hexadecimal characters. + NT_LAN_MANAGER_HEX_DIGEST_REGEXP = /[0-9a-f]{32}/i + # Valid format for an NTLM hash composed of `'<LAN Manager hex digest>:<NT LAN Manager hex digest>'`. + DATA_REGEXP = /\A#{LAN_MANAGER_HEX_DIGEST_REGEXP}:#{NT_LAN_MANAGER_HEX_DIGEST_REGEXP}\z/ + + # Takes a string and determines whether it is a valid NTLM Hash + # @param [String] the string to validate + # @return [Boolean] whether or not the string is a valid NTLM hash + def is_ntlm_hash?(data) + decoded_data = data.dup + decoded_data = EncodeUtil.decode_utf16le(decoded_data) + if DATA_REGEXP.match(decoded_data) + true + else + false + end + end + + # Conver the value to a 64-Bit Little Endian Int + # @param [String] val The string to convert + def pack_int64le(val) + [val & 0x00000000ffffffff, val >> 32].pack("V2") + end + + # Builds an array of strings that are 7 characters long + # @param [String] str The string to split + # @api private + def split7(str) + s = str.dup + until s.empty? + (ret ||= []).push s.slice!(0, 7) + end + ret + end + + # Not sure what this is doing + # @param [String] str String to generate keys for + # @api private + def gen_keys(str) + split7(str).map{ |str7| + bits = split7(str7.unpack("B*")[0]).inject('')\ + {|ret, tkn| ret += tkn + (tkn.gsub('1', '').size % 2).to_s } + [bits].pack("B*") + } + end + + def apply_des(plain, keys) + dec = OpenSSL::Cipher.new("des-cbc") + dec.padding = 0 + keys.map {|k| + dec.key = k + dec.encrypt.update(plain) + dec.final + } + end + + # Generates a Lan Manager Hash + # @param [String] password The password to base the hash on + def lm_hash(password) + keys = gen_keys password.upcase.ljust(14, "\0") + apply_des(LM_MAGIC, keys).join + end + + # Generate a NTLM Hash + # @param [String] password The password to base the hash on + # @option opt :unicode (false) Unicode encode the password + def ntlm_hash(password, opt = {}) + pwd = password.dup + unless opt[:unicode] + pwd = EncodeUtil.encode_utf16le(pwd) + end + OpenSSL::Digest::MD4.digest pwd + end + + # Generate a NTLMv2 Hash + # @param [String] user The username + # @param [String] password The password + # @param [String] target The domain or workstation to authenticate to + # @option opt :unicode (false) Unicode encode the domain + def ntlmv2_hash(user, password, target, opt={}) + if is_ntlm_hash? password + decoded_password = EncodeUtil.decode_utf16le(password) + ntlmhash = [decoded_password.upcase[33,65]].pack('H32') + else + ntlmhash = ntlm_hash(password, opt) + end + userdomain = user.upcase + target + unless opt[:unicode] + userdomain = EncodeUtil.encode_utf16le(userdomain) + end + OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, ntlmhash, userdomain) + end + + def lm_response(arg) + begin + hash = arg[:lm_hash] + chal = arg[:challenge] + rescue + raise ArgumentError + end + chal = NTLM::pack_int64le(chal) if chal.is_a?(Integer) + keys = gen_keys hash.ljust(21, "\0") + apply_des(chal, keys).join + end + + def ntlm_response(arg) + hash = arg[:ntlm_hash] + chal = arg[:challenge] + chal = NTLM::pack_int64le(chal) if chal.is_a?(Integer) + keys = gen_keys hash.ljust(21, "\0") + apply_des(chal, keys).join + end + + def ntlmv2_response(arg, opt = {}) + begin + key = arg[:ntlmv2_hash] + chal = arg[:challenge] + ti = arg[:target_info] + rescue + raise ArgumentError + end + chal = NTLM::pack_int64le(chal) if chal.is_a?(Integer) + + if opt[:client_challenge] + cc = opt[:client_challenge] + else + cc = rand(MAX64) + end + cc = NTLM::pack_int64le(cc) if cc.is_a?(Integer) + + if opt[:timestamp] + ts = opt[:timestamp] + else + ts = Time.now.to_i + end + # epoch -> milsec from Jan 1, 1601 + ts = 10_000_000 * (ts + TIME_OFFSET) + + blob = Blob.new + blob.timestamp = ts + blob.challenge = cc + blob.target_info = ti + + bb = blob.serialize + + OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, key, chal + bb) + bb + end + + def lmv2_response(arg, opt = {}) + key = arg[:ntlmv2_hash] + chal = arg[:challenge] + + chal = NTLM::pack_int64le(chal) if chal.is_a?(Integer) + + if opt[:client_challenge] + cc = opt[:client_challenge] + else + cc = rand(MAX64) + end + cc = NTLM::pack_int64le(cc) if cc.is_a?(Integer) + + OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, key, chal + cc) + cc + end + + def ntlm2_session(arg, opt = {}) + begin + passwd_hash = arg[:ntlm_hash] + chal = arg[:challenge] + rescue + raise ArgumentError + end + chal = NTLM::pack_int64le(chal) if chal.is_a?(Integer) + + if opt[:client_challenge] + cc = opt[:client_challenge] + else + cc = rand(MAX64) + end + cc = NTLM::pack_int64le(cc) if cc.is_a?(Integer) + + keys = gen_keys(passwd_hash.ljust(21, "\0")) + session_hash = OpenSSL::Digest::MD5.digest(chal + cc).slice(0, 8) + response = apply_des(session_hash, keys).join + [cc.ljust(24, "\0"), response] + end + end + + end +end