rubyntlm 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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