rubyntlm 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +2 -3
  3. data/README.md +9 -2
  4. data/Rakefile +8 -0
  5. data/examples/http.rb +1 -1
  6. data/lib/net/ntlm.rb +10 -9
  7. data/lib/net/ntlm/blob.rb +17 -6
  8. data/lib/net/ntlm/client.rb +61 -0
  9. data/lib/net/ntlm/client/session.rb +223 -0
  10. data/lib/net/ntlm/encode_util.rb +2 -2
  11. data/lib/net/ntlm/field_set.rb +9 -5
  12. data/lib/net/ntlm/message.rb +23 -9
  13. data/lib/net/ntlm/message/type1.rb +1 -26
  14. data/lib/net/ntlm/message/type2.rb +11 -37
  15. data/lib/net/ntlm/message/type3.rb +77 -14
  16. data/lib/net/ntlm/version.rb +1 -1
  17. data/rubyntlm.gemspec +3 -2
  18. data/spec/lib/net/ntlm/blob_spec.rb +1 -1
  19. data/spec/lib/net/ntlm/client/session_spec.rb +68 -0
  20. data/spec/lib/net/ntlm/client_spec.rb +64 -0
  21. data/spec/lib/net/ntlm/encode_util_spec.rb +3 -3
  22. data/spec/lib/net/ntlm/field_set_spec.rb +4 -4
  23. data/spec/lib/net/ntlm/field_spec.rb +5 -5
  24. data/spec/lib/net/ntlm/message/type1_spec.rb +99 -10
  25. data/spec/lib/net/ntlm/message/type2_spec.rb +68 -24
  26. data/spec/lib/net/ntlm/message/type3_spec.rb +207 -2
  27. data/spec/lib/net/ntlm/security_buffer_spec.rb +8 -8
  28. data/spec/lib/net/ntlm/string_spec.rb +14 -14
  29. data/spec/lib/net/ntlm/version_spec.rb +7 -6
  30. data/spec/lib/net/ntlm_spec.rb +20 -22
  31. data/spec/spec_helper.rb +1 -2
  32. data/spec/support/shared/examples/net/ntlm/field_shared.rb +3 -3
  33. data/spec/support/shared/examples/net/ntlm/fieldset_shared.rb +34 -34
  34. data/spec/support/shared/examples/net/ntlm/int_shared.rb +8 -8
  35. data/spec/support/shared/examples/net/ntlm/message_shared.rb +3 -3
  36. metadata +36 -16
@@ -24,8 +24,8 @@ module NTLM
24
24
  end
25
25
  else # Use native 1.9 string encoding functions
26
26
 
27
- # Decode a UTF16 string to a ASCII string
28
- # @param [String] str The string to convert
27
+ # Decode a UTF16 string to a ASCII string
28
+ # @param [String] str The string to convert
29
29
  def self.decode_utf16le(str)
30
30
  str.force_encoding(Encoding::UTF_16LE)
31
31
  str.encode(Encoding::UTF_8, Encoding::UTF_16LE).force_encoding('UTF-8')
@@ -88,12 +88,12 @@ module Net
88
88
  @alist = self.class.prototypes.map{ |n, t, o| [n, t.new(o)] }
89
89
  end
90
90
 
91
- def serialize
92
- @alist.map{|n, f| f.serialize }.join
91
+ def parse(str, offset=0)
92
+ @alist.inject(offset){|cur, a| cur += a[1].parse(str, cur)}
93
93
  end
94
94
 
95
- def parse(str, offset=0)
96
- @alist.inject(offset){|cur, a| cur += a[1].parse(str, cur)}
95
+ def serialize
96
+ @alist.map{|n, f| f.serialize }.join
97
97
  end
98
98
 
99
99
  def size
@@ -119,7 +119,11 @@ module Net
119
119
  def disable(name)
120
120
  self[name].active = false
121
121
  end
122
+
123
+ def has_disabled_fields?
124
+ @alist.any? { |name, field| !field.active }
125
+ end
122
126
  end
123
127
 
124
128
  end
125
- end
129
+ end
@@ -20,9 +20,10 @@ module NTLM
20
20
  :LOCAL_CALL => 0x00004000,
21
21
  :ALWAYS_SIGN => 0x00008000,
22
22
  :TARGET_TYPE_DOMAIN => 0x00010000,
23
- :TARGET_INFO => 0x00800000,
24
23
  :NTLM2_KEY => 0x00080000,
24
+ :TARGET_INFO => 0x00800000,
25
25
  :KEY128 => 0x20000000,
26
+ :KEY_EXCHANGE => 0x40000000,
26
27
  :KEY56 => 0x80000000
27
28
  }.freeze
28
29
 
@@ -42,14 +43,14 @@ module NTLM
42
43
  m = Type0.new
43
44
  m.parse(str)
44
45
  case m.type
45
- when 1
46
- t = Type1.parse(str)
47
- when 2
48
- t = Type2.parse(str)
49
- when 3
50
- t = Type3.parse(str)
51
- else
52
- raise ArgumentError, "unknown type: #{m.type}"
46
+ when 1
47
+ t = Type1.new.parse(str)
48
+ when 2
49
+ t = Type2.new.parse(str)
50
+ when 3
51
+ t = Type3.new.parse(str)
52
+ else
53
+ raise ArgumentError, "unknown type: #{m.type}"
53
54
  end
54
55
  t
55
56
  end
@@ -59,6 +60,19 @@ module NTLM
59
60
  end
60
61
  end
61
62
 
63
+ # @return [self]
64
+ def parse(str)
65
+ super
66
+
67
+ while has_disabled_fields? && serialize.size < str.size
68
+ # enable the next disabled field
69
+ self.class.names.find { |name| !self[name].active && enable(name) }
70
+ super
71
+ end
72
+
73
+ self
74
+ end
75
+
62
76
  def has_flag?(flag)
63
77
  (self[:flag].value & FLAGS[flag]) == FLAGS[flag]
64
78
  end
@@ -10,34 +10,9 @@ module Net
10
10
  int32LE :flag, {:value => DEFAULT_FLAGS[:TYPE1] }
11
11
  security_buffer :domain, {:value => ""}
12
12
  security_buffer :workstation, {:value => Socket.gethostname }
13
- string :padding, {:size => 0, :value => "", :active => false }
13
+ string :os_version, {:size => 8, :value => "", :active => false }
14
14
 
15
- class << Type1
16
- # Parses a Type 1 Message
17
- # @param [String] str A string containing Type 1 data
18
- # @return [Type1] The parsed Type 1 message
19
- def parse(str)
20
- t = new
21
- t.parse(str)
22
- t
23
- end
24
- end
25
-
26
- # @!visibility private
27
- def parse(str)
28
- super(str)
29
- enable(:domain) if has_flag?(:DOMAIN_SUPPLIED)
30
- enable(:workstation) if has_flag?(:WORKSTATION_SUPPLIED)
31
- super(str)
32
- if ( (len = data_edge - head_size) > 0)
33
- self.padding = "\0" * len
34
- super(str)
35
- end
36
- end
37
15
  end
38
-
39
16
  end
40
17
  end
41
18
  end
42
-
43
-
@@ -5,39 +5,14 @@ module Net
5
5
  # @private false
6
6
  class Type2 < Message
7
7
 
8
- string :sign, {:size => 8, :value => SSP_SIGN}
9
- int32LE :type, {:value => 2}
10
- security_buffer :target_name, {:size => 0, :value => ""}
11
- int32LE :flag, {:value => DEFAULT_FLAGS[:TYPE2]}
12
- int64LE :challenge, {:value => 0}
13
- int64LE :context, {:value => 0, :active => false}
14
- security_buffer :target_info, {:value => "", :active => false}
15
- string :padding, {:size => 0, :value => "", :active => false }
16
-
17
- class << Type2
18
- # Parse a Type 2 packet
19
- # @param [String] str A string containing Type 2 data
20
- # @return [Type2]
21
- def parse(str)
22
- t = new
23
- t.parse(str)
24
- t
25
- end
26
- end
27
-
28
- # @!visibility private
29
- def parse(str)
30
- super(str)
31
- if has_flag?(:TARGET_INFO)
32
- enable(:context)
33
- enable(:target_info)
34
- super(str)
35
- end
36
- if ( (len = data_edge - head_size) > 0)
37
- self.padding = "\0" * len
38
- super(str)
39
- end
40
- end
8
+ string :sign, { :size => 8, :value => SSP_SIGN }
9
+ int32LE :type, { :value => 2 }
10
+ security_buffer :target_name, { :size => 0, :value => "" }
11
+ int32LE :flag, { :value => DEFAULT_FLAGS[:TYPE2] }
12
+ int64LE :challenge, { :value => 0}
13
+ int64LE :context, { :value => 0, :active => false }
14
+ security_buffer :target_info, { :value => "", :active => false }
15
+ string :os_version, { :size => 8, :value => "", :active => false }
41
16
 
42
17
  # Generates a Type 3 response based on the Type 2 Information
43
18
  # @return [Type3]
@@ -45,9 +20,9 @@ module Net
45
20
  # @option arg [String] :password The user's password
46
21
  # @option arg [String] :domain ('') The domain to authenticate to
47
22
  # @option opt [String] :workstation (Socket.gethostname) The name of the calling workstation
48
- # @option opt [Boolean] :use_default_target (False) Use the domain supplied by the server in the Type 2 packet
23
+ # @option opt [Boolean] :use_default_target (false) Use the domain supplied by the server in the Type 2 packet
49
24
  # @note An empty :domain option authenticates to the local machine.
50
- # @note The :use_default_target has presidence over the :domain option
25
+ # @note The :use_default_target has precedence over the :domain option
51
26
  def response(arg, opt = {})
52
27
  usr = arg[:user]
53
28
  pwd = arg[:password]
@@ -115,9 +90,8 @@ module Net
115
90
  :flag => self.flag
116
91
  })
117
92
  end
118
- end
119
-
120
93
 
94
+ end
121
95
 
122
96
  end
123
97
  end
@@ -13,18 +13,10 @@ module Net
13
13
  security_buffer :user, {:value => ""}
14
14
  security_buffer :workstation, {:value => ""}
15
15
  security_buffer :session_key, {:value => "", :active => false }
16
- int64LE :flag, {:value => 0, :active => false }
16
+ int32LE :flag, {:value => 0, :active => false }
17
+ string :os_version, {:size => 8, :active => false }
17
18
 
18
19
  class << Type3
19
- # Parse a Type 3 packet
20
- # @param [String] str A string containing Type 3 data
21
- # @return [Type2]
22
- def parse(str)
23
- t = new
24
- t.parse(str)
25
- t
26
- end
27
-
28
20
  # Builds a Type 3 packet
29
21
  # @note All options must be properly encoded with either unicode or oem encoding
30
22
  # @return [Type3]
@@ -47,7 +39,7 @@ module Net
47
39
 
48
40
  if arg[:session_key]
49
41
  t.enable(:session_key)
50
- t.session_key = arg[session_key]
42
+ t.session_key = arg[:session_key]
51
43
  end
52
44
 
53
45
  if arg[:flag]
@@ -58,11 +50,82 @@ module Net
58
50
  t
59
51
  end
60
52
  end
61
- end
62
53
 
54
+ # @param server_challenge (see #password?)
55
+ def blank_password?(server_challenge)
56
+ password?('', server_challenge)
57
+ end
58
+
59
+ # @param password [String]
60
+ # @param server_challenge [String] The server's {Type2#challenge challenge} from the
61
+ # {Type2} message for which this object is a response.
62
+ # @return [true] if +password+ was the password used to generate this
63
+ # {Type3} message
64
+ # @return [false] otherwise
65
+ def password?(password, server_challenge)
66
+ case ntlm_version
67
+ when :ntlm2_session
68
+ ntlm2_session_password?(password, server_challenge)
69
+ when :ntlmv2
70
+ ntlmv2_password?(password, server_challenge)
71
+ else
72
+ raise
73
+ end
74
+ end
75
+
76
+ # @return [Symbol]
77
+ def ntlm_version
78
+ if ntlm_response.size == 24 && lm_response[0,8] != "\x00"*8 && lm_response[8,16] == "\x00"*16
79
+ :ntlm2_session
80
+ elsif ntlm_response.size == 24
81
+ :ntlmv1
82
+ elsif ntlm_response.size > 24
83
+ :ntlmv2
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ def ntlm2_session_password?(password, server_challenge)
90
+ hash = ntlm_response
91
+ _lm, empty_hash = NTLM.ntlm2_session(
92
+ {
93
+ :ntlm_hash => NTLM.ntlm_hash(password),
94
+ :challenge => server_challenge,
95
+ },
96
+ {
97
+ :client_challenge => lm_response[0,8]
98
+ }
99
+ )
100
+ hash == empty_hash
101
+ end
102
+
103
+ def ntlmv2_password?(password, server_challenge)
104
+
105
+ # The first 16 bytes of the ntlm_response are the HMAC of the blob
106
+ # that follows it.
107
+ blob = Blob.new
108
+ blob.parse(ntlm_response[16..-1])
63
109
 
110
+ empty_hash = NTLM.ntlmv2_response(
111
+ {
112
+ # user and domain came from the serialized data here, so
113
+ # they're already unicode
114
+ :ntlmv2_hash => NTLM.ntlmv2_hash(user, '', domain, :unicode => true),
115
+ :challenge => server_challenge,
116
+ :target_info => blob.target_info
117
+ },
118
+ {
119
+ :client_challenge => blob.challenge,
120
+ # The blob's timestamp is already in milliseconds since 1601,
121
+ # so convert it back to epoch time first
122
+ :timestamp => (blob.timestamp / 10_000_000) - NTLM::TIME_OFFSET,
123
+ }
124
+ )
125
+
126
+ empty_hash == ntlm_response
127
+ end
128
+ end
64
129
  end
65
130
  end
66
131
  end
67
-
68
-
@@ -3,7 +3,7 @@ module Net
3
3
  # @private
4
4
  module VERSION
5
5
  MAJOR = 0
6
- MINOR = 4
6
+ MINOR = 5
7
7
  TINY = 0
8
8
  STRING = [MAJOR, MINOR, TINY].join('.')
9
9
  end
data/rubyntlm.gemspec CHANGED
@@ -18,10 +18,11 @@ Gem::Specification.new do |s|
18
18
  s.require_paths = ["lib"]
19
19
 
20
20
  s.required_ruby_version = '>= 1.8.7'
21
-
21
+
22
22
  s.license = 'MIT'
23
23
 
24
+ s.add_development_dependency "pry"
24
25
  s.add_development_dependency "rake"
25
- s.add_development_dependency "rspec"
26
+ s.add_development_dependency "rspec", ">= 2.11"
26
27
  s.add_development_dependency "simplecov"
27
28
  end
@@ -13,4 +13,4 @@ describe Net::NTLM::Blob do
13
13
  ]
14
14
 
15
15
  it_behaves_like 'a fieldset', fields
16
- end
16
+ end
@@ -0,0 +1,68 @@
1
+ require 'spec_helper'
2
+
3
+ describe Net::NTLM::Client::Session do
4
+ let(:t2_challenge) { Net::NTLM::Message.decode64 "TlRMTVNTUAACAAAADAAMADgAAAA1goriAAyk1DmJUnUAAAAAAAAAAFAAUABEAAAABgLwIwAAAA9TAEUAUgBWAEUAUgACAAwAUwBFAFIAVgBFAFIAAQAMAFMARQBSAFYARQBSAAQADABzAGUAcgB2AGUAcgADAAwAcwBlAHIAdgBlAHIABwAIADd7mrNaB9ABAAAAAA==" }
5
+ let(:inst) { Net::NTLM::Client::Session.new(nil, t2_challenge) }
6
+ let(:user_session_key) {["3c4918ff0b33e2603e5d7ceaf34bb7d5"].pack("H*")}
7
+ let(:client_sign_key) {["f7f97a82ec390f9c903dac4f6aceb132"].pack("H*")}
8
+ let(:client_seal_key) {["6f0d99535033951cbe499cd1914fe9ee"].pack("H*")}
9
+ let(:server_sign_key) {["f7f97a82ec390f9c903dac4f6aceb132"].pack("H*")}
10
+ let(:server_seal_key) {["6f0d99535033951cbe499cd1914fe9ee"].pack("H*")}
11
+
12
+ describe "#sign_message" do
13
+
14
+ it "signs a message and when KEY_EXCHANGE is true" do
15
+ expect(inst).to receive(:client_sign_key).and_return(client_sign_key)
16
+ expect(inst).to receive(:client_seal_key).and_return(client_seal_key)
17
+ expect(inst).to receive(:negotiate_key_exchange?).and_return(true)
18
+ sm = inst.sign_message("Test Message")
19
+ str = "01000000b35ccd60c110c52f00000000"
20
+ expect(sm.unpack("H*")[0]).to eq(str)
21
+ end
22
+
23
+ end
24
+
25
+ describe "#verify_signature" do
26
+
27
+ it "verifies a message signature" do
28
+ expect(inst).to receive(:server_sign_key).and_return(server_sign_key)
29
+ expect(inst).to receive(:server_seal_key).and_return(server_seal_key)
30
+ expect(inst).to receive(:negotiate_key_exchange?).and_return(true)
31
+ sig = "01000000b35ccd60c110c52f00000000"
32
+ sm = inst.verify_signature([sig].pack("H*"), "Test Message")
33
+ expect(sm).to be true
34
+ end
35
+
36
+ end
37
+
38
+ describe "#seal_message" do
39
+ it "should seal the message" do
40
+ expect(inst).to receive(:client_seal_key).and_return(client_seal_key)
41
+ emsg = inst.seal_message("rubyntlm")
42
+ expect(emsg.unpack("H*")[0]).to eq("d7389b9604f6274f")
43
+ end
44
+ end
45
+
46
+ describe "#unseal_message" do
47
+ it "should unseal the message" do
48
+ expect(inst).to receive(:server_seal_key).and_return(server_seal_key)
49
+ msg = inst.unseal_message(["d7389b9604f6274f"].pack("H*"))
50
+ expect(msg).to eq("rubyntlm")
51
+ end
52
+ end
53
+
54
+ describe "#master_key" do
55
+ it "returns a random 16-byte key when negotiate_key_exchange? is true" do
56
+ expect(inst).to receive(:negotiate_key_exchange?).and_return(true)
57
+ expect(inst).not_to receive(:user_session_key)
58
+ inst.send :master_key
59
+ end
60
+
61
+ it "returns the user_session_key when negotiate_key_exchange? is false" do
62
+ expect(inst).to receive(:negotiate_key_exchange?).and_return(false)
63
+ expect(inst).to receive(:user_session_key).and_return(user_session_key)
64
+ inst.send :master_key
65
+ end
66
+ end
67
+
68
+ end
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+
3
+ describe Net::NTLM::Client do
4
+ let(:inst) { Net::NTLM::Client.new("test", "test01", :workstation => "testhost") }
5
+ let(:user_session_key) {["3c4918ff0b33e2603e5d7ceaf34bb7d5"].pack("H*")}
6
+
7
+ describe "#init_context" do
8
+
9
+ it "returns a default Type1 message" do
10
+ t1 = inst.init_context
11
+ expect(t1).to be_instance_of Net::NTLM::Message::Type1
12
+ expect(t1.domain).to eq("")
13
+ expect(t1.workstation).to eq("testhost")
14
+ expect(t1).to have_flag(:UNICODE)
15
+ expect(t1).to have_flag(:OEM)
16
+ expect(t1).to have_flag(:SIGN)
17
+ expect(t1).to have_flag(:SEAL)
18
+ expect(t1).to have_flag(:REQUEST_TARGET)
19
+ expect(t1).to have_flag(:NTLM)
20
+ expect(t1).to have_flag(:ALWAYS_SIGN)
21
+ expect(t1).to have_flag(:NTLM2_KEY)
22
+ expect(t1).to have_flag(:KEY128)
23
+ expect(t1).to have_flag(:KEY_EXCHANGE)
24
+ expect(t1).to have_flag(:KEY56)
25
+ end
26
+
27
+ it "clears session variable on new init_context" do
28
+ inst.instance_variable_set :@session, "BADSESSION"
29
+ expect(inst.session).to eq("BADSESSION")
30
+ inst.init_context
31
+ expect(inst.session).to be_nil
32
+ end
33
+
34
+ it "returns a Type1 message with custom flags" do
35
+ flags = Net::NTLM::FLAGS[:UNICODE] | Net::NTLM::FLAGS[:REQUEST_TARGET] | Net::NTLM::FLAGS[:NTLM]
36
+ inst = Net::NTLM::Client.new("test", "test01", :workstation => "testhost", :flags => flags)
37
+ t1 = inst.init_context
38
+ expect(t1).to be_instance_of Net::NTLM::Message::Type1
39
+ expect(t1.domain).to eq("")
40
+ expect(t1.workstation).to eq("testhost")
41
+ expect(t1).to have_flag(:UNICODE)
42
+ expect(t1).not_to have_flag(:OEM)
43
+ expect(t1).not_to have_flag(:SIGN)
44
+ expect(t1).not_to have_flag(:SEAL)
45
+ expect(t1).to have_flag(:REQUEST_TARGET)
46
+ expect(t1).to have_flag(:NTLM)
47
+ expect(t1).not_to have_flag(:ALWAYS_SIGN)
48
+ expect(t1).not_to have_flag(:NTLM2_KEY)
49
+ expect(t1).not_to have_flag(:KEY128)
50
+ expect(t1).not_to have_flag(:KEY_EXCHANGE)
51
+ expect(t1).not_to have_flag(:KEY56)
52
+ end
53
+
54
+ it "calls authenticate! when we receive a Challenge Message" do
55
+ t2_challenge = "TlRMTVNTUAACAAAADAAMADgAAAA1goriAAyk1DmJUnUAAAAAAAAAAFAAUABEAAAABgLwIwAAAA9TAEUAUgBWAEUAUgACAAwAUwBFAFIAVgBFAFIAAQAMAFMARQBSAFYARQBSAAQADABzAGUAcgB2AGUAcgADAAwAcwBlAHIAdgBlAHIABwAIADd7mrNaB9ABAAAAAA=="
56
+ session = double("session")
57
+ expect(session).to receive(:authenticate!)
58
+ expect(Net::NTLM::Client::Session).to receive(:new).with(inst, instance_of(Net::NTLM::Message::Type2)).and_return(session)
59
+ inst.init_context t2_challenge
60
+ end
61
+
62
+ end
63
+
64
+ end