net-imap 0.6.3 → 0.6.4

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.
@@ -691,6 +691,8 @@ module Net
691
691
  CRLF!
692
692
  EOF!
693
693
  resp
694
+ rescue SystemStackError
695
+ parse_error("response recursion too deep")
694
696
  end
695
697
 
696
698
  # RFC3501 & RFC9051:
@@ -1961,6 +1963,7 @@ module Net
1961
1963
  # resp-text-code =/ "UIDREQUIRED"
1962
1964
  def resp_text_code
1963
1965
  name = resp_text_code__name
1966
+ state = current_state
1964
1967
  data =
1965
1968
  case name
1966
1969
  when "CAPABILITY" then resp_code__capability
@@ -1983,8 +1986,18 @@ module Net
1983
1986
  when "MAILBOXID" then SP!; parens__objectid # RFC8474: OBJECTID
1984
1987
  when "UIDREQUIRED" then # RFC9586: UIDONLY
1985
1988
  else
1989
+ state = nil # don't backtrack
1986
1990
  SP? and text_chars_except_rbra
1987
1991
  end
1992
+ peek_rbra? or
1993
+ parse_error("expected resp-text-code %p to be complete", name)
1994
+ ResponseCode.new(name, data)
1995
+ rescue Net::IMAP::ResponseParseError => parse_error
1996
+ raise unless state
1997
+ raise if parse_error.message.include?("uid-set")
1998
+ restore_state state
1999
+ unparsed_data = SP? && text_chars_except_rbra
2000
+ data = InvalidParseData[parse_error:, unparsed_data:, parsed_data: data]
1988
2001
  ResponseCode.new(name, data)
1989
2002
  end
1990
2003
 
@@ -8,51 +8,60 @@ module Net
8
8
 
9
9
  def initialize(client, sock)
10
10
  @client, @sock = client, sock
11
+ # cached config
12
+ @max_response_size = nil
13
+ # response buffer state
14
+ @buff = @literal_size = nil
11
15
  end
12
16
 
13
17
  def read_response_buffer
18
+ @max_response_size = client.max_response_size
14
19
  @buff = String.new
15
20
  catch :eof do
16
21
  while true
22
+ guard_response_too_large!
17
23
  read_line
18
- break unless (@literal_size = get_literal_size)
24
+ # check before allocating memory for literal
25
+ guard_response_too_large!
26
+ break unless literal_size
19
27
  read_literal
20
28
  end
21
29
  end
22
30
  buff
23
31
  ensure
24
- @buff = nil
32
+ @buff = @literal_size = nil
25
33
  end
26
34
 
27
35
  private
28
36
 
37
+ # cached config
38
+ attr_reader :max_response_size
39
+
40
+ # response buffer state
29
41
  attr_reader :buff, :literal_size
30
42
 
31
43
  def bytes_read = buff.bytesize
32
44
  def empty? = buff.empty?
33
- def done? = line_done? && !get_literal_size
45
+ def done? = line_done? && !literal_size
34
46
  def line_done? = buff.end_with?(CRLF)
35
- def get_literal_size = /\{(\d+)\}\r\n\z/n =~ buff && $1.to_i
47
+
48
+ def get_literal_size(buff)
49
+ buff.end_with?("}\r\n") && buff.rindex(/\{(\d+)\}\r\n\z/n) && $1.to_i
50
+ end
36
51
 
37
52
  def read_line
38
- buff << (@sock.gets(CRLF, read_limit) or throw :eof)
39
- max_response_remaining! unless line_done?
53
+ line = (@sock.gets(CRLF, max_response_remaining) or throw :eof)
54
+ @literal_size = get_literal_size(line)
55
+ buff << line
40
56
  end
41
57
 
42
58
  def read_literal
43
- # check before allocating memory for literal
44
- max_response_remaining!
45
59
  literal = String.new(capacity: literal_size)
46
- buff << (@sock.read(read_limit(literal_size), literal) or throw :eof)
60
+ buff << (@sock.read(literal_size, literal) or throw :eof)
47
61
  ensure
48
62
  @literal_size = nil
49
63
  end
50
64
 
51
- def read_limit(limit = nil)
52
- [limit, max_response_remaining!].compact.min
53
- end
54
-
55
- def max_response_size = client.max_response_size
56
65
  def max_response_remaining = max_response_size &.- bytes_read
57
66
  def response_too_large? = max_response_size &.< min_response_size
58
67
  def min_response_size = bytes_read + min_response_remaining
@@ -61,8 +70,8 @@ module Net
61
70
  empty? ? 3 : done? ? 0 : (literal_size || 0) + 2
62
71
  end
63
72
 
64
- def max_response_remaining!
65
- return max_response_remaining unless response_too_large?
73
+ def guard_response_too_large!
74
+ return unless response_too_large?
66
75
  raise ResponseTooLargeError.new(
67
76
  max_response_size:, bytes_read:, literal_size:,
68
77
  )
@@ -75,13 +75,19 @@ module Net
75
75
  # * #password ― Password or passphrase associated with this #username.
76
76
  # * _optional_ #authzid ― Alternate identity to act as or on behalf of.
77
77
  # * _optional_ #min_iterations - Overrides the default value (4096).
78
+ # * _optional_ #max_iterations - Overrides the default value (2³¹ - 1).
78
79
  #
79
80
  # Any other keyword parameters are quietly ignored.
81
+ #
82
+ # *NOTE:* <em>It is the user's responsibility</em> to enforce minimum
83
+ # and maximum iteration counts that are appropriate for their security
84
+ # context.
80
85
  def initialize(username_arg = nil, password_arg = nil,
81
86
  authcid: nil, username: nil,
82
87
  authzid: nil,
83
88
  password: nil, secret: nil,
84
89
  min_iterations: 4096, # see both RFC5802 and RFC7677
90
+ max_iterations: 2**31 - 1, # max int32
85
91
  cnonce: nil, # must only be set in tests
86
92
  **options)
87
93
  @username = username || username_arg || authcid or
@@ -94,7 +100,22 @@ module Net
94
100
  @min_iterations.positive? or
95
101
  raise ArgumentError, "min_iterations must be positive"
96
102
 
103
+ @max_iterations = Integer max_iterations.to_int
104
+ @min_iterations <= @max_iterations or
105
+ raise ArgumentError, "max_iterations must be more than min_iterations"
106
+
97
107
  @cnonce = cnonce || SecureRandom.base64(32)
108
+
109
+ # These attrs are set from the server challenges
110
+ @server_first_message = @snonce = @salt = @iterations = nil
111
+ @server_error = nil
112
+
113
+ # Memoized after @salt and @iterations have been sent.
114
+ @salted_password = @client_key = @server_key = nil
115
+
116
+ # These values are created and cached in response to server challenges
117
+ @client_first_message_bare = nil
118
+ @client_final_message_without_proof = nil
98
119
  end
99
120
 
100
121
  # Authentication identity: the identity that matches the #password.
@@ -127,8 +148,43 @@ module Net
127
148
 
128
149
  # The minimal allowed iteration count. Lower #iterations will raise an
129
150
  # Error.
151
+ #
152
+ # *WARNING:* The default value (4096) is set to match guidance from
153
+ # both {RFC5802}[https://www.rfc-editor.org/rfc/rfc5802#page-12]
154
+ # and RFC7677[https://www.rfc-editor.org/rfc/rfc7677#section-4], but
155
+ # {modern recommendations}[https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2]
156
+ # are significantly higher.
157
+ #
158
+ # It is ultimately the server's responsibility to securely store
159
+ # password hashes. While this parameter can alert the user to
160
+ # insecure password storage and prevent insecure authentication
161
+ # exchange, updating the iteration count generally requires resetting
162
+ # the password on the server.
130
163
  attr_reader :min_iterations
131
164
 
165
+ # The maximal allowed iteration count. Higher #iterations will raise an
166
+ # Error.
167
+ #
168
+ # As noted in {RFC5802}[https://www.rfc-editor.org/rfc/rfc5802#section-9]
169
+ # >>>
170
+ # A hostile server can perform a computational denial-of-service
171
+ # attack on clients by sending a big iteration count value.
172
+ #
173
+ # *WARNING:* The default value is <tt>2³¹ - 1</tt>, the maximum signed
174
+ # 32-bit integer. This is large enough for the computation to take
175
+ # several minutes, and insufficient protection against hostile servers.
176
+ #
177
+ # Note that <tt>OpenSSL::KDF.pbkdf2_hmac</tt> is implemented by a
178
+ # blocking C function, and cannot be interrupted by +Timeout+ or
179
+ # <tt>Thread.raise</tt>. And it keeps the Global VM lock, as of v4.0 of
180
+ # the +openssl+ gem, so other ruby threads will not be able to run.
181
+ #
182
+ # <em>To prevent a denial of service attack,</em> this must be set to a
183
+ # safe value, depending on hardware and version of OpenSSL. <em>It is
184
+ # the user's responsibility</em> to enforce minimum and maximum
185
+ # iteration counts that are appropriate for their security context.
186
+ attr_reader :max_iterations
187
+
132
188
  # The client nonce, generated by SecureRandom
133
189
  attr_reader :cnonce
134
190
 
@@ -147,6 +203,15 @@ module Net
147
203
  # Net::IMAP::NoResponseError.
148
204
  attr_reader :server_error
149
205
 
206
+ # Memoized ScramAlgorithm#salted_password (needs #salt and #iterations)
207
+ def salted_password = @salted_password ||= compute_salted { super }
208
+
209
+ # Memoized ScramAlgorithm#client_key (needs #salt and #iterations)
210
+ def client_key = @client_key ||= compute_salted { super }
211
+
212
+ # Memoized ScramAlgorithm#server_key (needs #salt and #iterations)
213
+ def server_key = @server_key ||= compute_salted { super }
214
+
150
215
  # Returns a new OpenSSL::Digest object, set to the appropriate hash
151
216
  # function for the chosen mechanism.
152
217
  #
@@ -186,6 +251,13 @@ module Net
186
251
 
187
252
  private
188
253
 
254
+ # Checks for +salt+ and +iterations+ before yielding
255
+ def compute_salted
256
+ salt in String or raise Error, "unknown salt"
257
+ iterations in Integer or raise Error, "unknown iterations"
258
+ yield
259
+ end
260
+
189
261
  # Need to store this for auth_message
190
262
  attr_reader :server_first_message
191
263
 
@@ -202,6 +274,8 @@ module Net
202
274
  raise Error, "server did not send iteration count"
203
275
  min_iterations <= iterations or
204
276
  raise Error, "too few iterations: %d" % [iterations]
277
+ max_iterations.nil? || iterations <= max_iterations or
278
+ raise Error, "too many iterations: %d" % [iterations]
205
279
  mext = sparams["m"] and
206
280
  raise Error, "mandatory extension: %p" % [mext]
207
281
  snonce.start_with? cnonce or
@@ -123,7 +123,11 @@ module Net
123
123
  # Net::IMAP::SearchResult[9, 1, 2, 4, 10, 12, 3, modseq: 123_456]
124
124
  # .to_sequence_set
125
125
  # # => Net::IMAP::SequenceSet["1:4,9:10,12"]
126
- def to_sequence_set; SequenceSet[*self] end
126
+ #
127
+ # >>>
128
+ # *NOTE:* +SORT+ order is not preserved. The result will be sorted.
129
+ #
130
+ def to_sequence_set; empty? ? SequenceSet.empty : SequenceSet[to_a] end
127
131
 
128
132
  def pretty_print(pp)
129
133
  return super if modseq.nil?