lib/net/ntlm.rb in rubyntlm-0.3.4 vs lib/net/ntlm.rb in rubyntlm-0.4.0
- old
+ new
@@ -8,13 +8,10 @@
# and Minero Aoki. You can find original code here:
# http://jp.rubyist.net/magazine/?0013-CodeReview
# -------------------------------------------------------------
# Copyright (c) 2005,2006 yrock
#
-# This program is free software.
-# You can distribute/modify this program under the terms of the
-# Ruby License.
#
# 2006-02-11 refactored by Minero Aoki
# -------------------------------------------------------------
#
# All protocol information used to write this code stems from
@@ -23,14 +20,10 @@
# available on the net.
# http://davenport.sourceforge.net/ntlm.html
# -------------------------------------------------------------
# Copyright (c) 2003 Eric Glass
#
-# Permission to use, copy, modify, and distribute this document
-# for any purpose and without any fee is hereby granted,
-# provided that the above copyright notice and this list of
-# conditions appear in all copies.
# -------------------------------------------------------------
#
# 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.
@@ -46,100 +39,40 @@
require 'base64'
require 'openssl'
require 'openssl/digest'
require 'socket'
-module Net
- module NTLM
- # @private
- module VERSION
- MAJOR = 0
- MINOR = 3
- TINY = 4
- STRING = [MAJOR, MINOR, TINY].join('.')
- end
+# Load Order is important here
+require 'net/ntlm/field'
+require 'net/ntlm/int16_le'
+require 'net/ntlm/int32_le'
+require 'net/ntlm/int64_le'
+require 'net/ntlm/string'
- SSP_SIGN = "NTLMSSP\0"
- BLOB_SIGN = 0x00000101
- LM_MAGIC = "KGS!@\#$%"
- TIME_OFFSET = 11644473600
- MAX64 = 0xffffffffffffffff
+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'
- FLAGS = {
- :UNICODE => 0x00000001,
- :OEM => 0x00000002,
- :REQUEST_TARGET => 0x00000004,
- :MBZ9 => 0x00000008,
- :SIGN => 0x00000010,
- :SEAL => 0x00000020,
- :NEG_DATAGRAM => 0x00000040,
- :NETWARE => 0x00000100,
- :NTLM => 0x00000200,
- :NEG_NT_ONLY => 0x00000400,
- :MBZ7 => 0x00000800,
- :DOMAIN_SUPPLIED => 0x00001000,
- :WORKSTATION_SUPPLIED => 0x00002000,
- :LOCAL_CALL => 0x00004000,
- :ALWAYS_SIGN => 0x00008000,
- :TARGET_TYPE_DOMAIN => 0x00010000,
- :TARGET_INFO => 0x00800000,
- :NTLM2_KEY => 0x00080000,
- :KEY128 => 0x20000000,
- :KEY56 => 0x80000000
- }.freeze
- FLAG_KEYS = FLAGS.keys.sort{|a, b| FLAGS[a] <=> FLAGS[b] }
+require 'net/ntlm/encode_util'
- DEFAULT_FLAGS = {
- :TYPE1 => FLAGS[:UNICODE] | FLAGS[:OEM] | FLAGS[:REQUEST_TARGET] | FLAGS[:NTLM] | FLAGS[:ALWAYS_SIGN] | FLAGS[:NTLM2_KEY],
- :TYPE2 => FLAGS[:UNICODE],
- :TYPE3 => FLAGS[:UNICODE] | FLAGS[:REQUEST_TARGET] | FLAGS[:NTLM] | FLAGS[:ALWAYS_SIGN] | FLAGS[:NTLM2_KEY]
- }
- class EncodeUtil
- if RUBY_VERSION == "1.8.7"
- require "kconv"
- # Decode a UTF16 string to a ASCII string
- # @param [String] str The string to convert
- def self.decode_utf16le(str)
- Kconv.kconv(swap16(str), Kconv::ASCII, Kconv::UTF16)
- end
- # Encodes a ASCII string to a UTF16 string
- # @param [String] str The string to convert
- def self.encode_utf16le(str)
- swap16(Kconv.kconv(str, Kconv::UTF16, Kconv::ASCII))
- end
+module Net
+ module NTLM
- # Taggle the strings endianness between big/little and little/big
- # @param [String] str The string to swap the endianness on
- def self.swap16(str)
- str.unpack("v*").pack("n*")
- end
- else # Use native 1.9 string encoding functions
+ LM_MAGIC = "KGS!@\#$%"
+ TIME_OFFSET = 11644473600
+ MAX64 = 0xffffffffffffffff
- # Decode a UTF16 string to a ASCII string
- # @param [String] str The string to convert
- def self.decode_utf16le(str)
- str.encode(Encoding::UTF_8, Encoding::UTF_16LE).force_encoding('UTF-8')
- end
- # Encodes a ASCII string to a UTF16 string
- # @param [String] str The string to convert
- # @note This implementation may seem stupid but the problem is that UTF16-LE and UTF-8 are incompatiable
- # encodings. This library uses string contatination to build the packet bytes. The end result is that
- # you can either marshal the encodings elsewhere of simply know that each time you call encode_utf16le
- # the function will convert the string bytes to UTF-16LE and note the encoding as UTF-8 so that byte
- # concatination works seamlessly.
- def self.encode_utf16le(str)
- str = str.force_encoding('UTF-8') if [::Encoding::ASCII_8BIT,::Encoding::US_ASCII].include?(str.encoding)
- str.dup.force_encoding('UTF-8').encode(Encoding::UTF_16LE, Encoding::UTF_8).force_encoding('UTF-8')
- end
- end
- end
-
class << self
# Conver the value to a 64-Bit Little Endian Int
# @param [String] val The string to convert
def pack_int64le(val)
@@ -298,572 +231,7 @@
response = apply_des(session_hash, keys).join
[cc.ljust(24, "\0"), response]
end
end
-
- # base classes for primitives
- # @private
- class Field
- attr_accessor :active, :value
-
- def initialize(opts)
- @value = opts[:value]
- @active = opts[:active].nil? ? true : opts[:active]
- end
-
- def size
- @active ? @size : 0
- end
- end
-
- class String < Field
- def initialize(opts)
- super(opts)
- @size = opts[:size]
- end
-
- def parse(str, offset=0)
- if @active and str.size >= offset + @size
- @value = str[offset, @size]
- @size
- else
- 0
- end
- end
-
- def serialize
- if @active
- @value
- else
- ""
- end
- end
-
- def value=(val)
- @value = val
- @size = @value.nil? ? 0 : @value.size
- @active = (@size > 0)
- end
- end
-
- class Int16LE < Field
- def initialize(opt)
- super(opt)
- @size = 2
- end
- def parse(str, offset=0)
- if @active and str.size >= offset + @size
- @value = str[offset, @size].unpack("v")[0]
- @size
- else
- 0
- end
- end
-
- def serialize
- [@value].pack("v")
- end
- end
-
- class Int32LE < Field
- def initialize(opt)
- super(opt)
- @size = 4
- end
-
- def parse(str, offset=0)
- if @active and str.size >= offset + @size
- @value = str.slice(offset, @size).unpack("V")[0]
- @size
- else
- 0
- end
- end
-
- def serialize
- [@value].pack("V") if @active
- end
- end
-
- class Int64LE < Field
- def initialize(opt)
- super(opt)
- @size = 8
- end
-
- def parse(str, offset=0)
- if @active and str.size >= offset + @size
- d, u = str.slice(offset, @size).unpack("V2")
- @value = (u * 0x100000000 + d)
- @size
- else
- 0
- end
- end
-
- def serialize
- [@value & 0x00000000ffffffff, @value >> 32].pack("V2") if @active
- end
- end
-
- # base class of data structure
- class FieldSet
- class << FieldSet
-
-
- # @macro string_security_buffer
- # @method $1
- # @method $1=
- # @return [String]
- def string(name, opts)
- add_field(name, String, opts)
- end
-
- # @macro int16le_security_buffer
- # @method $1
- # @method $1=
- # @return [Int16LE]
- def int16LE(name, opts)
- add_field(name, Int16LE, opts)
- end
-
- # @macro int32le_security_buffer
- # @method $1
- # @method $1=
- # @return [Int32LE]
- def int32LE(name, opts)
- add_field(name, Int32LE, opts)
- end
-
- # @macro int64le_security_buffer
- # @method $1
- # @method $1=
- # @return [Int64]
- def int64LE(name, opts)
- add_field(name, Int64LE, opts)
- end
-
- # @macro security_buffer
- # @method $1
- # @method $1=
- # @return [SecurityBuffer]
- def security_buffer(name, opts)
- add_field(name, SecurityBuffer, opts)
- end
-
- def prototypes
- @proto
- end
-
- def names
- @proto.map{|n, t, o| n}
- end
-
- def types
- @proto.map{|n, t, o| t}
- end
-
- def opts
- @proto.map{|n, t, o| o}
- end
-
- private
-
- def add_field(name, type, opts)
- (@proto ||= []).push [name, type, opts]
- define_accessor name
- end
-
- def define_accessor(name)
- module_eval(<<-End, __FILE__, __LINE__ + 1)
- def #{name}
- self['#{name}'].value
- end
-
- def #{name}=(val)
- self['#{name}'].value = val
- end
- End
- end
- end
-
- def initialize
- @alist = self.class.prototypes.map{ |n, t, o| [n, t.new(o)] }
- end
-
- def serialize
- @alist.map{|n, f| f.serialize }.join
- end
-
- def parse(str, offset=0)
- @alist.inject(offset){|cur, a| cur += a[1].parse(str, cur)}
- end
-
- def size
- @alist.inject(0){|sum, a| sum += a[1].size}
- end
-
- def [](name)
- a = @alist.assoc(name.to_s.intern)
- raise ArgumentError, "no such field: #{name}" unless a
- a[1]
- end
-
- def []=(name, val)
- a = @alist.assoc(name.to_s.intern)
- raise ArgumentError, "no such field: #{name}" unless a
- a[1] = val
- end
-
- def enable(name)
- self[name].active = true
- end
-
- def disable(name)
- self[name].active = false
- end
- end
-
- class Blob < FieldSet
- int32LE :blob_signature, {:value => BLOB_SIGN}
- int32LE :reserved, {:value => 0}
- int64LE :timestamp, {:value => 0}
- string :challenge, {:value => "", :size => 8}
- int32LE :unknown1, {:value => 0}
- string :target_info, {:value => "", :size => 0}
- int32LE :unknown2, {:value => 0}
- end
-
- class SecurityBuffer < FieldSet
-
- int16LE :length, {:value => 0}
- int16LE :allocated, {:value => 0}
- int32LE :offset, {:value => 0}
-
- attr_accessor :active
- def initialize(opts)
- super()
- @value = opts[:value]
- @active = opts[:active].nil? ? true : opts[:active]
- @size = 8
- end
-
- def parse(str, offset=0)
- if @active and str.size >= offset + @size
- super(str, offset)
- @value = str[self.offset, self.length]
- @size
- else
- 0
- end
- end
-
- def serialize
- super if @active
- end
-
- def value
- @value
- end
-
- def value=(val)
- @value = val
- self.length = self.allocated = val.size
- end
-
- def data_size
- @active ? @value.size : 0
- end
- end
-
- # @private false
- class Message < FieldSet
- class << Message
- def parse(str)
- m = Type0.new
- m.parse(str)
- case m.type
- when 1
- t = Type1.parse(str)
- when 2
- t = Type2.parse(str)
- when 3
- t = Type3.parse(str)
- else
- raise ArgumentError, "unknown type: #{m.type}"
- end
- t
- end
-
- def decode64(str)
- parse(Base64.decode64(str))
- end
- end
-
- def has_flag?(flag)
- (self[:flag].value & FLAGS[flag]) == FLAGS[flag]
- end
-
- def set_flag(flag)
- self[:flag].value |= FLAGS[flag]
- end
-
- def dump_flags
- FLAG_KEYS.each{ |k| print(k, "=", flag?(k), "\n") }
- end
-
- def serialize
- deflag
- super + security_buffers.map{|n, f| f.value}.join
- end
-
- def encode64
- Base64.encode64(serialize).gsub(/\n/, '')
- end
-
- def decode64(str)
- parse(Base64.decode64(str))
- end
-
- alias head_size size
-
- def data_size
- security_buffers.inject(0){|sum, a| sum += a[1].data_size}
- end
-
- def size
- head_size + data_size
- end
-
-
- def security_buffers
- @alist.find_all{|n, f| f.instance_of?(SecurityBuffer)}
- end
-
- def deflag
- security_buffers.inject(head_size){|cur, a|
- a[1].offset = cur
- cur += a[1].data_size
- }
- end
-
- def data_edge
- security_buffers.map{ |n, f| f.active ? f.offset : size}.min
- end
-
- # sub class definitions
- class Type0 < Message
- string :sign, {:size => 8, :value => SSP_SIGN}
- int32LE :type, {:value => 0}
- end
-
- # @private false
- class Type1 < Message
-
- string :sign, {:size => 8, :value => SSP_SIGN}
- int32LE :type, {:value => 1}
- int32LE :flag, {:value => DEFAULT_FLAGS[:TYPE1] }
- security_buffer :domain, {:value => ""}
- security_buffer :workstation, {:value => Socket.gethostname }
- string :padding, {:size => 0, :value => "", :active => false }
-
- class << Type1
- # Parses a Type 1 Message
- # @param [String] str A string containing Type 1 data
- # @return [Type1] The parsed Type 1 message
- def parse(str)
- t = new
- t.parse(str)
- t
- end
- end
-
- # @!visibility private
- def parse(str)
- super(str)
- enable(:domain) if has_flag?(:DOMAIN_SUPPLIED)
- enable(:workstation) if has_flag?(:WORKSTATION_SUPPLIED)
- super(str)
- if ( (len = data_edge - head_size) > 0)
- self.padding = "\0" * len
- super(str)
- end
- end
- end
-
-
- # @private false
- class Type2 < Message
-
- string :sign, {:size => 8, :value => SSP_SIGN}
- int32LE :type, {:value => 2}
- security_buffer :target_name, {:size => 0, :value => ""}
- int32LE :flag, {:value => DEFAULT_FLAGS[:TYPE2]}
- int64LE :challenge, {:value => 0}
- int64LE :context, {:value => 0, :active => false}
- security_buffer :target_info, {:value => "", :active => false}
- string :padding, {:size => 0, :value => "", :active => false }
-
- class << Type2
- # Parse a Type 2 packet
- # @param [String] str A string containing Type 2 data
- # @return [Type2]
- def parse(str)
- t = new
- t.parse(str)
- t
- end
- end
-
- # @!visibility private
- def parse(str)
- super(str)
- if has_flag?(:TARGET_INFO)
- enable(:context)
- enable(:target_info)
- super(str)
- end
- if ( (len = data_edge - head_size) > 0)
- self.padding = "\0" * len
- super(str)
- end
- end
-
- # Generates a Type 3 response based on the Type 2 Information
- # @return [Type3]
- # @option arg [String] :username The username to authenticate with
- # @option arg [String] :password The user's password
- # @option arg [String] :domain ('') The domain to authenticate to
- # @option opt [String] :workstation (Socket.gethostname) The name of the calling workstation
- # @option opt [Boolean] :use_default_target (False) Use the domain supplied by the server in the Type 2 packet
- # @note An empty :domain option authenticates to the local machine.
- # @note The :use_default_target has presidence over the :domain option
- def response(arg, opt = {})
- usr = arg[:user]
- pwd = arg[:password]
- domain = arg[:domain] ? arg[:domain] : ""
- if usr.nil? or pwd.nil?
- raise ArgumentError, "user and password have to be supplied"
- end
-
- if opt[:workstation]
- ws = opt[:workstation]
- else
- ws = Socket.gethostname
- end
-
- if opt[:client_challenge]
- cc = opt[:client_challenge]
- else
- cc = rand(MAX64)
- end
- cc = NTLM::pack_int64le(cc) if cc.is_a?(Integer)
- opt[:client_challenge] = cc
-
- if has_flag?(:OEM) and opt[:unicode]
- usr = NTLM::EncodeUtil.decode_utf16le(usr)
- pwd = NTLM::EncodeUtil.decode_utf16le(pwd)
- ws = NTLM::EncodeUtil.decode_utf16le(ws)
- domain = NTLM::EncodeUtil.decode_utf16le(domain)
- opt[:unicode] = false
- end
-
- if has_flag?(:UNICODE) and !opt[:unicode]
- usr = NTLM::EncodeUtil.encode_utf16le(usr)
- pwd = NTLM::EncodeUtil.encode_utf16le(pwd)
- ws = NTLM::EncodeUtil.encode_utf16le(ws)
- domain = NTLM::EncodeUtil.encode_utf16le(domain)
- opt[:unicode] = true
- end
-
- if opt[:use_default_target]
- domain = self.target_name
- end
-
- ti = self.target_info
-
- chal = self[:challenge].serialize
-
- if opt[:ntlmv2]
- ar = {:ntlmv2_hash => NTLM::ntlmv2_hash(usr, pwd, domain, opt), :challenge => chal, :target_info => ti}
- lm_res = NTLM::lmv2_response(ar, opt)
- ntlm_res = NTLM::ntlmv2_response(ar, opt)
- elsif has_flag?(:NTLM2_KEY)
- ar = {:ntlm_hash => NTLM::ntlm_hash(pwd, opt), :challenge => chal}
- lm_res, ntlm_res = NTLM::ntlm2_session(ar, opt)
- else
- lm_res = NTLM::lm_response(pwd, chal)
- ntlm_res = NTLM::ntlm_response(pwd, chal)
- end
-
- Type3.create({
- :lm_response => lm_res,
- :ntlm_response => ntlm_res,
- :domain => domain,
- :user => usr,
- :workstation => ws,
- :flag => self.flag
- })
- end
- end
-
- # @private false
- class Type3 < Message
-
- string :sign, {:size => 8, :value => SSP_SIGN}
- int32LE :type, {:value => 3}
- security_buffer :lm_response, {:value => ""}
- security_buffer :ntlm_response, {:value => ""}
- security_buffer :domain, {:value => ""}
- security_buffer :user, {:value => ""}
- security_buffer :workstation, {:value => ""}
- security_buffer :session_key, {:value => "", :active => false }
- int64LE :flag, {:value => 0, :active => false }
-
- class << Type3
- # Parse a Type 3 packet
- # @param [String] str A string containing Type 3 data
- # @return [Type2]
- def parse(str)
- t = new
- t.parse(str)
- t
- end
-
- # Builds a Type 3 packet
- # @note All options must be properly encoded with either unicode or oem encoding
- # @return [Type3]
- # @option arg [String] :lm_response The LM hash
- # @option arg [String] :ntlm_response The NTLM hash
- # @option arg [String] :domain The domain to authenticate to
- # @option arg [String] :workstation The name of the calling workstation
- # @option arg [String] :session_key The session key
- # @option arg [Integer] :flag Flags for the packet
- def create(arg, opt ={})
- t = new
- t.lm_response = arg[:lm_response]
- t.ntlm_response = arg[:ntlm_response]
- t.domain = arg[:domain]
- t.user = arg[:user]
-
- if arg[:workstation]
- t.workstation = arg[:workstation]
- end
-
- if arg[:session_key]
- t.enable(:session_key)
- t.session_key = arg[session_key]
- end
-
- if arg[:flag]
- t.enable(:session_key)
- t.enable(:flag)
- t.flag = arg[:flag]
- end
- t
- end
- end
- end
- end
end
end