rubyntlm 0.6.6 → 0.6.7

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/README.md +48 -48
  4. data/lib/net/ntlm/blob.rb +11 -10
  5. data/lib/net/ntlm/channel_binding.rb +13 -9
  6. data/lib/net/ntlm/client/session.rb +34 -141
  7. data/lib/net/ntlm/client/session_crypto.rb +126 -0
  8. data/lib/net/ntlm/client.rb +8 -9
  9. data/lib/net/ntlm/encode_util.rb +39 -38
  10. data/lib/net/ntlm/exceptions.rb +3 -0
  11. data/lib/net/ntlm/field.rb +29 -31
  12. data/lib/net/ntlm/field_set.rb +39 -18
  13. data/lib/net/ntlm/int16_le.rb +8 -8
  14. data/lib/net/ntlm/int32_le.rb +8 -7
  15. data/lib/net/ntlm/int64_le.rb +8 -7
  16. data/lib/net/ntlm/md4.rb +102 -64
  17. data/lib/net/ntlm/message/type0.rb +4 -7
  18. data/lib/net/ntlm/message/type1.rb +8 -9
  19. data/lib/net/ntlm/message/type2.rb +87 -67
  20. data/lib/net/ntlm/message/type3.rb +43 -41
  21. data/lib/net/ntlm/message.rb +116 -111
  22. data/lib/net/ntlm/rc4.rb +51 -44
  23. data/lib/net/ntlm/response.rb +112 -0
  24. data/lib/net/ntlm/security_buffer.rb +13 -15
  25. data/lib/net/ntlm/string.rb +8 -7
  26. data/lib/net/ntlm/target_info.rb +52 -44
  27. data/lib/net/ntlm/version.rb +3 -1
  28. data/lib/net/ntlm.rb +37 -125
  29. data/lib/rubyntlm.rb +3 -1
  30. metadata +5 -44
  31. data/.devcontainer/devcontainer.json +0 -22
  32. data/.github/dependabot.yml +0 -10
  33. data/.github/workflows/build.yml +0 -55
  34. data/.gitignore +0 -11
  35. data/.rspec +0 -3
  36. data/Gemfile +0 -20
  37. data/Rakefile +0 -37
  38. data/examples/http.rb +0 -86
  39. data/examples/imap.rb +0 -73
  40. data/examples/smtp.rb +0 -94
  41. data/rubyntlm.gemspec +0 -28
  42. data/spec/lib/net/ntlm/blob_spec.rb +0 -14
  43. data/spec/lib/net/ntlm/channel_binding_spec.rb +0 -15
  44. data/spec/lib/net/ntlm/client/session_spec.rb +0 -85
  45. data/spec/lib/net/ntlm/client_spec.rb +0 -62
  46. data/spec/lib/net/ntlm/encode_util_spec.rb +0 -14
  47. data/spec/lib/net/ntlm/field_set_spec.rb +0 -31
  48. data/spec/lib/net/ntlm/field_spec.rb +0 -32
  49. data/spec/lib/net/ntlm/int16_le_spec.rb +0 -16
  50. data/spec/lib/net/ntlm/int32_le_spec.rb +0 -17
  51. data/spec/lib/net/ntlm/int64_le_spec.rb +0 -17
  52. data/spec/lib/net/ntlm/md4_spec.rb +0 -16
  53. data/spec/lib/net/ntlm/message/type0_spec.rb +0 -19
  54. data/spec/lib/net/ntlm/message/type1_spec.rb +0 -129
  55. data/spec/lib/net/ntlm/message/type2_spec.rb +0 -131
  56. data/spec/lib/net/ntlm/message/type3_spec.rb +0 -336
  57. data/spec/lib/net/ntlm/message_spec.rb +0 -15
  58. data/spec/lib/net/ntlm/security_buffer_spec.rb +0 -62
  59. data/spec/lib/net/ntlm/string_spec.rb +0 -70
  60. data/spec/lib/net/ntlm/target_info_spec.rb +0 -74
  61. data/spec/lib/net/ntlm/version_spec.rb +0 -26
  62. data/spec/lib/net/ntlm_spec.rb +0 -133
  63. data/spec/spec_helper.rb +0 -51
  64. data/spec/support/certificates/sha_256_hash.pem +0 -19
  65. data/spec/support/shared/examples/net/ntlm/field_shared.rb +0 -25
  66. data/spec/support/shared/examples/net/ntlm/fieldset_shared.rb +0 -239
  67. data/spec/support/shared/examples/net/ntlm/int_shared.rb +0 -43
  68. data/spec/support/shared/examples/net/ntlm/message_shared.rb +0 -35
data/lib/net/ntlm/rc4.rb CHANGED
@@ -1,59 +1,66 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'openssl'
2
4
 
3
5
  module Net
4
- module NTLM
5
-
6
- begin
7
- OpenSSL::Cipher.new("rc4")
8
- rescue
9
- # libssl-3.0+ doesn't support legacy Rc4 -> use our own implementation
10
-
11
- class Rc4
12
- def initialize(str)
13
- raise ArgumentError, "RC4: Key supplied is blank" if str.eql?('')
14
- initialize_state(str)
15
- @q1, @q2 = 0, 0
16
- end
6
+ # NTLM cryptographic primitives.
7
+ module NTLM
8
+ begin
9
+ OpenSSL::Cipher.new('rc4')
10
+ rescue StandardError
11
+ # libssl-3.0+ doesn't support legacy Rc4 -> use our own implementation
17
12
 
18
- def encrypt(text)
19
- text.each_byte.map do |b|
20
- @q1 = (@q1 + 1) % 256
21
- @q2 = (@q2 + @state[@q1]) % 256
22
- @state[@q1], @state[@q2] = @state[@q2], @state[@q1]
23
- b ^ @state[(@state[@q1] + @state[@q2]) % 256]
24
- end.pack("C*")
25
- end
13
+ # Pure-Ruby RC4 implementation, used when OpenSSL does not provide RC4.
14
+ class Rc4
15
+ def initialize(str)
16
+ raise ArgumentError, 'RC4: Key supplied is blank' if str.eql?('')
17
+
18
+ initialize_state(str)
19
+ @q1 = 0
20
+ @q2 = 0
21
+ end
22
+
23
+ def encrypt(text)
24
+ text.each_byte.map { |byte| keystream_byte ^ byte }.pack('C*')
25
+ end
26
26
 
27
- private
27
+ private
28
28
 
29
- # The initial state which is then modified by the key-scheduling algorithm
30
- INITIAL_STATE = (0..255).to_a
29
+ # The initial state which is then modified by the key-scheduling algorithm
30
+ INITIAL_STATE = (0..255).to_a
31
31
 
32
- # Performs the key-scheduling algorithm to initialize the state.
33
- def initialize_state(key)
34
- i = j = 0
35
- @state = INITIAL_STATE.dup
36
- key_length = key.length
37
- while i < 256
38
- j = (j + @state[i] + key.getbyte(i % key_length)) % 256
39
- @state[i], @state[j] = @state[j], @state[i]
40
- i += 1
32
+ # Advances the cipher state and returns the next keystream byte.
33
+ def keystream_byte
34
+ @q1 = (@q1 + 1) % 256
35
+ @q2 = (@q2 + @state[@q1]) % 256
36
+ @state[@q1], @state[@q2] = @state[@q2], @state[@q1]
37
+ @state[(@state[@q1] + @state[@q2]) % 256]
41
38
  end
42
- end
43
- end
44
39
 
45
- else
46
- # Openssl/libssl provides RC4, so we can use it.
47
- class Rc4
48
- def initialize(str)
49
- @ci = OpenSSL::Cipher.new("rc4")
50
- @ci.key = str
40
+ # Performs the key-scheduling algorithm to initialize the state.
41
+ def initialize_state(key)
42
+ i = j = 0
43
+ @state = INITIAL_STATE.dup
44
+ key_length = key.length
45
+ while i < 256
46
+ j = (j + @state[i] + key.getbyte(i % key_length)) % 256
47
+ @state[i], @state[j] = @state[j], @state[i]
48
+ i += 1
49
+ end
50
+ end
51
51
  end
52
+ else
53
+ # Openssl/libssl provides RC4, so we can use it.
54
+ class Rc4
55
+ def initialize(str)
56
+ @ci = OpenSSL::Cipher.new('rc4')
57
+ @ci.key = str
58
+ end
52
59
 
53
- def encrypt(text)
54
- @ci.update(text) + @ci.final
60
+ def encrypt(text)
61
+ @ci.update(text) + @ci.final
62
+ end
55
63
  end
56
64
  end
57
65
  end
58
66
  end
59
- end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ # NTLM response builders: LM, NTLM, NTLMv2, LMv2 and NTLM2 session responses.
5
+ module NTLM
6
+ class << self
7
+ # Builds an LM response for the given challenge
8
+ # @param [Hash] arg :lm_hash, :challenge
9
+ def lm_response(arg)
10
+ hash, chal = lm_args(arg)
11
+ keys = gen_keys hash.ljust(21, "\0")
12
+ apply_des(pack_challenge(chal), keys).join
13
+ end
14
+
15
+ # Builds an NTLM response for the given challenge
16
+ # @param [Hash] arg :ntlm_hash, :challenge
17
+ def ntlm_response(arg)
18
+ hash = arg[:ntlm_hash]
19
+ chal = arg[:challenge]
20
+ chal = NTLM.pack_int64le(chal) if chal.is_a?(Integer)
21
+ keys = gen_keys hash.ljust(21, "\0")
22
+ apply_des(chal, keys).join
23
+ end
24
+
25
+ # Builds an NTLMv2 response for the given challenge
26
+ # @param [Hash] arg :ntlmv2_hash, :challenge, :target_info
27
+ # @option opt [String] :client_challenge 8-byte client challenge
28
+ # @option opt [Integer] :timestamp Time of the response
29
+ def ntlmv2_response(arg, opt = {})
30
+ key, chal, ti = ntlmv2_args(arg)
31
+ cc = client_challenge(opt)
32
+ bb = ntlmv2_blob(ti, opt[:timestamp], cc).serialize
33
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('MD5'), key, pack_challenge(chal) + bb) + bb
34
+ end
35
+
36
+ # Builds an LMv2 response for the given challenge
37
+ # @param [Hash] arg :ntlmv2_hash, :challenge
38
+ # @option opt [String] :client_challenge 8-byte client challenge
39
+ def lmv2_response(arg, opt = {})
40
+ key = arg[:ntlmv2_hash]
41
+ chal = arg[:challenge]
42
+
43
+ chal = NTLM.pack_int64le(chal) if chal.is_a?(Integer)
44
+
45
+ cc = opt[:client_challenge] || rand(MAX64)
46
+ cc = NTLM.pack_int64le(cc) if cc.is_a?(Integer)
47
+
48
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('MD5'), key, chal + cc) + cc
49
+ end
50
+
51
+ # Builds an NTLM2 session response for the given challenge
52
+ # @param [Hash] arg :ntlm_hash, :challenge
53
+ # @option opt [String] :client_challenge 8-byte client challenge
54
+ def ntlm2_session(arg, opt = {})
55
+ passwd_hash, chal = ntlm2_session_args(arg)
56
+ cc = client_challenge(opt)
57
+ keys = gen_keys(passwd_hash.ljust(21, "\0"))
58
+ session_hash = OpenSSL::Digest::MD5.digest(pack_challenge(chal) + cc).slice(0, 8)
59
+ response = apply_des(session_hash, keys).join
60
+ [cc.ljust(24, "\0"), response]
61
+ end
62
+
63
+ private
64
+
65
+ # @api private
66
+ def lm_args(arg)
67
+ [arg[:lm_hash], arg[:challenge]]
68
+ rescue StandardError
69
+ raise ArgumentError
70
+ end
71
+
72
+ # @api private
73
+ def ntlmv2_args(arg)
74
+ [arg[:ntlmv2_hash], arg[:challenge], arg[:target_info]]
75
+ rescue StandardError
76
+ raise ArgumentError
77
+ end
78
+
79
+ # @api private
80
+ def ntlm2_session_args(arg)
81
+ [arg[:ntlm_hash], arg[:challenge]]
82
+ rescue StandardError
83
+ raise ArgumentError
84
+ end
85
+
86
+ # @api private
87
+ def client_challenge(opt)
88
+ cc = opt[:client_challenge] || rand(MAX64)
89
+ cc = NTLM.pack_int64le(cc) if cc.is_a?(Integer)
90
+ cc
91
+ end
92
+
93
+ # @api private
94
+ def pack_challenge(chal)
95
+ chal.is_a?(Integer) ? NTLM.pack_int64le(chal) : chal
96
+ end
97
+
98
+ # @api private
99
+ def ntlmv2_blob(target_info, timestamp, challenge)
100
+ ts = timestamp || Time.now.to_i
101
+ # epoch -> milsec from Jan 1, 1601
102
+ ts = 10_000_000 * (ts + TIME_OFFSET)
103
+
104
+ blob = Blob.new
105
+ blob.timestamp = ts
106
+ blob.challenge = challenge
107
+ blob.target_info = target_info
108
+ blob
109
+ end
110
+ end
111
+ end
112
+ end
@@ -1,25 +1,26 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module Net
3
4
  module NTLM
4
-
5
+ # NTLMSSP security buffer: length/allocated/offset triplet plus payload.
5
6
  class SecurityBuffer < FieldSet
6
-
7
- int16LE :length, {:value => 0}
8
- int16LE :allocated, {:value => 0}
9
- int32LE :offset, {:value => 0}
7
+ int16_le :length, { value: 0 }
8
+ int16_le :allocated, { value: 0 }
9
+ int32_le :offset, { value: 0 }
10
10
 
11
11
  attr_accessor :active
12
- def initialize(opts={})
12
+
13
+ def initialize(opts = {})
13
14
  super()
14
15
  @value = opts[:value]
15
- @active = opts[:active].nil? ? true : opts[:active]
16
+ @active = opts[:active].nil? || opts[:active]
16
17
  @size = 8
17
18
  end
18
19
 
19
- def parse(str, offset=0)
20
- if @active and str.size >= offset + @size
20
+ def parse(str, offset = 0)
21
+ if @active && str.size >= offset + @size
21
22
  super(str, offset)
22
- @value = str[self.offset, self.length]
23
+ @value = str[self.offset, length]
23
24
  @size
24
25
  else
25
26
  0
@@ -30,9 +31,7 @@ module Net
30
31
  super if @active
31
32
  end
32
33
 
33
- def value
34
- @value
35
- end
34
+ attr_reader :value
36
35
 
37
36
  def value=(val)
38
37
  @value = val
@@ -43,6 +42,5 @@ module Net
43
42
  @active ? @value.size : 0
44
43
  end
45
44
  end
46
-
47
45
  end
48
- end
46
+ end
@@ -1,14 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Net
2
4
  module NTLM
3
-
5
+ # Fixed-size string field.
4
6
  class String < Field
5
7
  def initialize(opts)
6
8
  super(opts)
7
9
  @size = opts[:size]
8
10
  end
9
11
 
10
- def parse(str, offset=0)
11
- if @active and str.size >= offset + @size
12
+ def parse(str, offset = 0)
13
+ if @active && str.size >= offset + @size
12
14
  @value = str[offset, @size]
13
15
  @size
14
16
  else
@@ -20,16 +22,15 @@ module Net
20
22
  if @active
21
23
  @value.to_s
22
24
  else
23
- ""
25
+ ''
24
26
  end
25
27
  end
26
28
 
27
29
  def value=(val)
28
30
  @value = val
29
31
  @size = @value.nil? ? 0 : @value.size
30
- @active = (@size > 0)
32
+ @active = @size.positive?
31
33
  end
32
34
  end
33
-
34
35
  end
35
- end
36
+ end
@@ -1,22 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Net
2
4
  module NTLM
3
-
4
5
  # Represents a list of AV_PAIR structures
5
6
  # @see https://msdn.microsoft.com/en-us/library/cc236646.aspx
6
7
  class TargetInfo
7
-
8
8
  # Allowed AvId values for an AV_PAIR
9
- MSV_AV_EOL = "\x00\x00".freeze
10
- MSV_AV_NB_COMPUTER_NAME = "\x01\x00".freeze
11
- MSV_AV_NB_DOMAIN_NAME = "\x02\x00".freeze
12
- MSV_AV_DNS_COMPUTER_NAME = "\x03\x00".freeze
13
- MSV_AV_DNS_DOMAIN_NAME = "\x04\x00".freeze
14
- MSV_AV_DNS_TREE_NAME = "\x05\x00".freeze
15
- MSV_AV_FLAGS = "\x06\x00".freeze
16
- MSV_AV_TIMESTAMP = "\x07\x00".freeze
17
- MSV_AV_SINGLE_HOST = "\x08\x00".freeze
18
- MSV_AV_TARGET_NAME = "\x09\x00".freeze
19
- MSV_AV_CHANNEL_BINDINGS = "\x0A\x00".freeze
9
+ MSV_AV_EOL = "\x00\x00"
10
+ MSV_AV_NB_COMPUTER_NAME = "\x01\x00"
11
+ MSV_AV_NB_DOMAIN_NAME = "\x02\x00"
12
+ MSV_AV_DNS_COMPUTER_NAME = "\x03\x00"
13
+ MSV_AV_DNS_DOMAIN_NAME = "\x04\x00"
14
+ MSV_AV_DNS_TREE_NAME = "\x05\x00"
15
+ MSV_AV_FLAGS = "\x06\x00"
16
+ MSV_AV_TIMESTAMP = "\x07\x00"
17
+ MSV_AV_SINGLE_HOST = "\x08\x00"
18
+ MSV_AV_TARGET_NAME = "\x09\x00"
19
+ MSV_AV_CHANNEL_BINDINGS = "\x0A\x00"
20
20
 
21
21
  # @param av_pair_sequence [String] AV_PAIR list from challenge message
22
22
  def initialize(av_pair_sequence)
@@ -26,8 +26,8 @@ module Net
26
26
  attr_reader :av_pairs
27
27
 
28
28
  def to_s
29
- result = ''
30
- av_pairs.each do |k,v|
29
+ result = +''
30
+ av_pairs.each do |k, v|
31
31
  result << k
32
32
  result << [v.length].pack('S')
33
33
  result << v
@@ -40,49 +40,57 @@ module Net
40
40
  private
41
41
 
42
42
  VALID_PAIR_ID = [
43
- MSV_AV_EOL,
44
- MSV_AV_NB_COMPUTER_NAME,
45
- MSV_AV_NB_DOMAIN_NAME,
46
- MSV_AV_DNS_COMPUTER_NAME,
47
- MSV_AV_DNS_DOMAIN_NAME,
48
- MSV_AV_DNS_TREE_NAME,
49
- MSV_AV_FLAGS,
50
- MSV_AV_TIMESTAMP,
51
- MSV_AV_SINGLE_HOST,
52
- MSV_AV_TARGET_NAME,
53
- MSV_AV_CHANNEL_BINDINGS
43
+ MSV_AV_EOL,
44
+ MSV_AV_NB_COMPUTER_NAME,
45
+ MSV_AV_NB_DOMAIN_NAME,
46
+ MSV_AV_DNS_COMPUTER_NAME,
47
+ MSV_AV_DNS_DOMAIN_NAME,
48
+ MSV_AV_DNS_TREE_NAME,
49
+ MSV_AV_FLAGS,
50
+ MSV_AV_TIMESTAMP,
51
+ MSV_AV_SINGLE_HOST,
52
+ MSV_AV_TARGET_NAME,
53
+ MSV_AV_CHANNEL_BINDINGS
54
54
  ].freeze
55
55
 
56
56
  def read_pairs(av_pair_sequence)
57
- offset = 0
58
57
  result = {}
59
58
  return result if av_pair_sequence.nil?
60
59
 
61
- until offset >= av_pair_sequence.length
62
- id = av_pair_sequence[offset..offset+1]
60
+ read_all_pairs(av_pair_sequence, result)
63
61
 
64
- unless VALID_PAIR_ID.include?(id)
65
- raise Net::NTLM::InvalidTargetDataError.new(
66
- "Invalid AvId #{to_hex(id)} in AV_PAIR structure",
67
- av_pair_sequence
68
- )
69
- end
70
-
71
- length = av_pair_sequence[offset+2..offset+3].unpack('S')[0].to_i
72
- if length > 0
73
- value = av_pair_sequence[offset+4..offset+4+length-1]
74
- result[id] = value
75
- end
62
+ result
63
+ end
76
64
 
77
- offset += 4 + length
65
+ def read_all_pairs(sequence, result)
66
+ offset = 0
67
+ loop do
68
+ offset = read_pair(sequence, offset, result)
69
+ break if offset >= sequence.length
78
70
  end
71
+ end
79
72
 
80
- result
73
+ def read_pair(sequence, offset, result)
74
+ id = sequence[offset..offset + 1]
75
+ validate_pair_id(id, sequence)
76
+ length = sequence[offset + 2..offset + 3].unpack1('S').to_i
77
+ result[id] = sequence[offset + 4, length] if length.positive?
78
+ offset + 4 + length
79
+ end
80
+
81
+ def validate_pair_id(id, sequence)
82
+ return if VALID_PAIR_ID.include?(id)
83
+
84
+ raise Net::NTLM::InvalidTargetDataError.new(
85
+ "Invalid AvId #{to_hex(id)} in AV_PAIR structure",
86
+ sequence
87
+ )
81
88
  end
82
89
 
83
90
  def to_hex(str)
84
91
  return nil if str.nil?
85
- str.bytes.map {|b| '0x' + b.to_s(16).rjust(2,'0').upcase}.join('-')
92
+
93
+ str.bytes.map { |b| "0x#{b.to_s(16).rjust(2, '0').upcase}" }.join('-')
86
94
  end
87
95
  end
88
96
  end
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Net
2
4
  module NTLM
3
5
  # @private
4
6
  module VERSION
5
7
  MAJOR = 0
6
8
  MINOR = 6
7
- TINY = 6
9
+ TINY = 7
8
10
  STRING = [MAJOR, MINOR, TINY].join('.')
9
11
  end
10
12
  end