net-imap 0.6.3 → 0.6.4.1
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.
- checksums.yaml +4 -4
- data/.document +3 -0
- data/.rdoc_options +7 -0
- data/Gemfile +1 -1
- data/README.md +1 -1
- data/lib/net/imap/command_data.rb +223 -25
- data/lib/net/imap/config.rb +34 -0
- data/lib/net/imap/data_encoding.rb +57 -6
- data/lib/net/imap/errors.rb +47 -10
- data/lib/net/imap/response_data.rb +108 -11
- data/lib/net/imap/response_parser.rb +25 -12
- data/lib/net/imap/response_reader.rb +30 -16
- data/lib/net/imap/sasl/scram_authenticator.rb +74 -0
- data/lib/net/imap/search_result.rb +5 -1
- data/lib/net/imap.rb +209 -75
- data/rakelib/rdoc.rake +1 -18
- metadata +4 -2
|
@@ -75,9 +75,18 @@ module Net
|
|
|
75
75
|
#
|
|
76
76
|
# Net::IMAP::UnparsedData represents data for unknown response types or
|
|
77
77
|
# unknown extensions to response types without a well-defined extension
|
|
78
|
-
# grammar.
|
|
78
|
+
# grammar. UnparsedData represents the portion of the response which the
|
|
79
|
+
# parser has skipped over, without attempting to parse it.
|
|
79
80
|
#
|
|
80
|
-
#
|
|
81
|
+
# parser = Net::IMAP::ResponseParser.new
|
|
82
|
+
# response = parser.parse "* X-UNKNOWN-TYPE can't parse this\r\n"
|
|
83
|
+
# response => Net::IMAP::UntaggedResponse(
|
|
84
|
+
# name: "X-UNKNOWN-TYPE",
|
|
85
|
+
# data: Net::IMAP::UnparsedData(unparsed_data: "can't parse this"),
|
|
86
|
+
# )
|
|
87
|
+
#
|
|
88
|
+
# See also: UnparsedNumericResponseData, ExtensionData, IgnoredResponse,
|
|
89
|
+
# InvalidParseData.
|
|
81
90
|
class UnparsedData < Struct.new(:unparsed_data)
|
|
82
91
|
##
|
|
83
92
|
# method: unparsed_data
|
|
@@ -86,6 +95,61 @@ module Net
|
|
|
86
95
|
# The unparsed data
|
|
87
96
|
end
|
|
88
97
|
|
|
98
|
+
# **Note:** This represents an intentionally _unstable_ API. Where
|
|
99
|
+
# instances of this class are returned, future releases may return a
|
|
100
|
+
# different (incompatible) object <em>without deprecation or warning</em>.
|
|
101
|
+
#
|
|
102
|
+
# When the response parser encounters a recoverable error,
|
|
103
|
+
# Net::IMAP::InvalidParseData represents that portion of the response which
|
|
104
|
+
# could not be parsed, allowing the parser to parse the remainder of the
|
|
105
|
+
# response. InvalidParseData is always associated with a ResponseParseError
|
|
106
|
+
# which has been rescued.
|
|
107
|
+
#
|
|
108
|
+
# This could be caused by a malformed server response, by a bug in
|
|
109
|
+
# Net::IMAP::ResponseParser, or by an unsupported extension to the response
|
|
110
|
+
# syntax. For example, if a server supports +UIDPLUS+, but sends an invalid
|
|
111
|
+
# +COPYUID+ response code:
|
|
112
|
+
#
|
|
113
|
+
# parser = Net::IMAP::ResponseParser.new
|
|
114
|
+
# parsed = parser.parse "* OK [COPYUID 701 ] copied one message\r\n"
|
|
115
|
+
# parsed => {
|
|
116
|
+
# data: Net::IMAP::ResponseText(
|
|
117
|
+
# code: Net::IMAP::ResponseCode(
|
|
118
|
+
# name: "COPYUID",
|
|
119
|
+
# data: Net::IMAP::InvalidParseData(
|
|
120
|
+
# parse_error: Net::IMAP::ResponseParseError,
|
|
121
|
+
# unparsed_data: "701 ",
|
|
122
|
+
# parsed_data: nil,
|
|
123
|
+
# )
|
|
124
|
+
# )
|
|
125
|
+
# )
|
|
126
|
+
# }
|
|
127
|
+
#
|
|
128
|
+
# In this example, although <tt>[COPYUID 701 ]</tt> uses valid syntax for a
|
|
129
|
+
# _generic_ ResponseCode, it is _invalid_ syntax for a +COPYUID+ response
|
|
130
|
+
# code.
|
|
131
|
+
#
|
|
132
|
+
# See also: UnparsedData, ExtensionData
|
|
133
|
+
class InvalidParseData < Data.define(:parse_error, :unparsed_data, :parsed_data)
|
|
134
|
+
##
|
|
135
|
+
# method: parse_error
|
|
136
|
+
# :call-seq: parse_error -> ResponseParseError
|
|
137
|
+
#
|
|
138
|
+
# Returns the rescued ResponseParseError.
|
|
139
|
+
|
|
140
|
+
##
|
|
141
|
+
# method: unparsed_data
|
|
142
|
+
# :call-seq: unparsed_data -> string
|
|
143
|
+
#
|
|
144
|
+
# Returns the raw string which was skipped over by the parser.
|
|
145
|
+
|
|
146
|
+
##
|
|
147
|
+
# method: parsed_data
|
|
148
|
+
#
|
|
149
|
+
# May return a partial parse result for unparsed_data, which had already
|
|
150
|
+
# been parsed before the parse_error.
|
|
151
|
+
end
|
|
152
|
+
|
|
89
153
|
# **Note:** This represents an intentionally _unstable_ API. Where
|
|
90
154
|
# instances of this class are returned, future releases may return a
|
|
91
155
|
# different (incompatible) object <em>without deprecation or warning</em>.
|
|
@@ -93,7 +157,17 @@ module Net
|
|
|
93
157
|
# Net::IMAP::UnparsedNumericResponseData represents data for unhandled
|
|
94
158
|
# response types with a numeric prefix. See the documentation for #number.
|
|
95
159
|
#
|
|
96
|
-
#
|
|
160
|
+
# parser = Net::IMAP::ResponseParser.new
|
|
161
|
+
# response = parser.parse "* 123 X-UNKNOWN-TYPE can't parse this\r\n"
|
|
162
|
+
# response => Net::IMAP::UntaggedResponse(
|
|
163
|
+
# name: "X-UNKNOWN-TYPE",
|
|
164
|
+
# data: Net::IMAP::UnparsedNumericData(
|
|
165
|
+
# number: 123,
|
|
166
|
+
# unparsed_data: "can't parse this"
|
|
167
|
+
# ),
|
|
168
|
+
# )
|
|
169
|
+
#
|
|
170
|
+
# See also: UnparsedData, ExtensionData, IgnoredResponse, InvalidParseData
|
|
97
171
|
class UnparsedNumericResponseData < Struct.new(:number, :unparsed_data)
|
|
98
172
|
##
|
|
99
173
|
# method: number
|
|
@@ -306,6 +380,14 @@ module Net
|
|
|
306
380
|
# because the server doesn't allow deletion of mailboxes with children.
|
|
307
381
|
# #data is +nil+.
|
|
308
382
|
#
|
|
383
|
+
# === <tt>QUOTA=RES-*</tt> response codes
|
|
384
|
+
# See {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208.html#section-4.3].
|
|
385
|
+
# * +OVERQUOTA+ (also in RFC5530[https://www.rfc-editor.org/rfc/rfc5530]),
|
|
386
|
+
# with a tagged +NO+ response to an +APPEND+/+COPY+/+MOVE+ command when
|
|
387
|
+
# the command would put the target mailbox over any quota, and with an
|
|
388
|
+
# untagged +NO+ when a mailbox exceeds a soft quota (which may be caused
|
|
389
|
+
# be external events). #data is +nil+.
|
|
390
|
+
#
|
|
309
391
|
# === +CONDSTORE+ extension
|
|
310
392
|
# See {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
|
|
311
393
|
# * +NOMODSEQ+, when selecting a mailbox that does not support
|
|
@@ -324,9 +406,10 @@ module Net
|
|
|
324
406
|
#
|
|
325
407
|
# Response codes are backwards compatible: Servers are allowed to send new
|
|
326
408
|
# response codes even if the client has not enabled the extension that
|
|
327
|
-
# defines them. When
|
|
328
|
-
# code
|
|
329
|
-
#
|
|
409
|
+
# defines them. When ResponseParser does not know how to parse the response
|
|
410
|
+
# code data, #data may return the unparsed string, ExtensionData, or
|
|
411
|
+
# UnparsedData. When ResponseParser attempts but fails to parse the
|
|
412
|
+
# response code data, #data returns InvalidParseData.
|
|
330
413
|
class ResponseCode < Struct.new(:name, :data)
|
|
331
414
|
##
|
|
332
415
|
# method: name
|
|
@@ -341,8 +424,13 @@ module Net
|
|
|
341
424
|
#
|
|
342
425
|
# Returns the parsed response code data, e.g: an array of capabilities
|
|
343
426
|
# strings, an array of character set strings, a list of permanent flags,
|
|
344
|
-
# an Integer, etc. The response #
|
|
345
|
-
# code data can take.
|
|
427
|
+
# an Integer, etc. The response #name determines what form the response
|
|
428
|
+
# code #data can take.
|
|
429
|
+
#
|
|
430
|
+
# When ResponseParser does not know how to parse the response code data,
|
|
431
|
+
# #data may return the unparsed string, ExtensionData, or UnparsedData.
|
|
432
|
+
# When ResponseParser attempts but fails to parse the response code data,
|
|
433
|
+
# #data returns InvalidParseData.
|
|
346
434
|
end
|
|
347
435
|
|
|
348
436
|
# MailboxList represents the data of an untagged +LIST+ response, for a
|
|
@@ -383,14 +471,23 @@ module Net
|
|
|
383
471
|
# and MailboxQuota objects.
|
|
384
472
|
#
|
|
385
473
|
# == Required capability
|
|
474
|
+
#
|
|
386
475
|
# Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
|
|
387
|
-
#
|
|
476
|
+
# or <tt>QUOTA=RES-STORAGE</tt>
|
|
477
|
+
# [RFC9208[https://www.rfc-editor.org/rfc/rfc9208]] capability.
|
|
388
478
|
class MailboxQuota < Struct.new(:mailbox, :usage, :quota)
|
|
389
479
|
##
|
|
390
480
|
# method: mailbox
|
|
391
481
|
# :call-seq: mailbox -> string
|
|
392
482
|
#
|
|
393
|
-
# The
|
|
483
|
+
# The quota root with the associated quota.
|
|
484
|
+
#
|
|
485
|
+
# NOTE: this was mistakenly named "mailbox". But the quota root's name may
|
|
486
|
+
# differ from the mailbox. A single quota root may cover multiple
|
|
487
|
+
# mailboxes, and a single mailbox may be governed by multiple quota roots.
|
|
488
|
+
|
|
489
|
+
# The quota root with the associated quota.
|
|
490
|
+
alias quota_root mailbox
|
|
394
491
|
|
|
395
492
|
##
|
|
396
493
|
# method: usage
|
|
@@ -402,7 +499,7 @@ module Net
|
|
|
402
499
|
# method: quota
|
|
403
500
|
# :call-seq: quota -> Integer
|
|
404
501
|
#
|
|
405
|
-
#
|
|
502
|
+
# Storage limit imposed on the mailbox.
|
|
406
503
|
#
|
|
407
504
|
end
|
|
408
505
|
|
|
@@ -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
|
|
|
@@ -2199,10 +2212,7 @@ module Net
|
|
|
2199
2212
|
if $1
|
|
2200
2213
|
return Token.new(T_SPACE, $+)
|
|
2201
2214
|
elsif $2
|
|
2202
|
-
|
|
2203
|
-
val = @str[@pos, len]
|
|
2204
|
-
@pos += len
|
|
2205
|
-
return Token.new(T_LITERAL8, val)
|
|
2215
|
+
literal_token($+, T_LITERAL8)
|
|
2206
2216
|
elsif $3 && $7
|
|
2207
2217
|
# greedily match ATOM, prefixed with NUMBER, NIL, or PLUS.
|
|
2208
2218
|
return Token.new(T_ATOM, $3)
|
|
@@ -2230,10 +2240,7 @@ module Net
|
|
|
2230
2240
|
elsif $15
|
|
2231
2241
|
return Token.new(T_RBRA, $+)
|
|
2232
2242
|
elsif $16
|
|
2233
|
-
|
|
2234
|
-
val = @str[@pos, len]
|
|
2235
|
-
@pos += len
|
|
2236
|
-
return Token.new(T_LITERAL, val)
|
|
2243
|
+
literal_token($+)
|
|
2237
2244
|
elsif $17
|
|
2238
2245
|
return Token.new(T_PERCENT, $+)
|
|
2239
2246
|
elsif $18
|
|
@@ -2259,10 +2266,7 @@ module Net
|
|
|
2259
2266
|
elsif $4
|
|
2260
2267
|
return Token.new(T_QUOTED, Patterns.unescape_quoted($+))
|
|
2261
2268
|
elsif $5
|
|
2262
|
-
|
|
2263
|
-
val = @str[@pos, len]
|
|
2264
|
-
@pos += len
|
|
2265
|
-
return Token.new(T_LITERAL, val)
|
|
2269
|
+
literal_token($+)
|
|
2266
2270
|
elsif $6
|
|
2267
2271
|
return Token.new(T_LPAR, $+)
|
|
2268
2272
|
elsif $7
|
|
@@ -2277,6 +2281,15 @@ module Net
|
|
|
2277
2281
|
else
|
|
2278
2282
|
parse_error("invalid @lex_state - %s", @lex_state.inspect)
|
|
2279
2283
|
end
|
|
2284
|
+
rescue DataFormatError => error
|
|
2285
|
+
parse_error error.message
|
|
2286
|
+
end
|
|
2287
|
+
|
|
2288
|
+
def literal_token(len, type = T_LITERAL)
|
|
2289
|
+
len = NumValidator.coerce_number64 len
|
|
2290
|
+
val = @str[@pos, len]
|
|
2291
|
+
@pos += len
|
|
2292
|
+
Token.new(type, val)
|
|
2280
2293
|
end
|
|
2281
2294
|
|
|
2282
2295
|
end
|
|
@@ -4,55 +4,69 @@ module Net
|
|
|
4
4
|
class IMAP
|
|
5
5
|
# See https://www.rfc-editor.org/rfc/rfc9051#section-2.2.2
|
|
6
6
|
class ResponseReader # :nodoc:
|
|
7
|
+
include NumValidator
|
|
8
|
+
|
|
7
9
|
attr_reader :client
|
|
8
10
|
|
|
9
11
|
def initialize(client, sock)
|
|
10
12
|
@client, @sock = client, sock
|
|
13
|
+
# cached config
|
|
14
|
+
@max_response_size = nil
|
|
15
|
+
# response buffer state
|
|
16
|
+
@buff = @literal_size = nil
|
|
11
17
|
end
|
|
12
18
|
|
|
13
19
|
def read_response_buffer
|
|
20
|
+
@max_response_size = client.max_response_size
|
|
14
21
|
@buff = String.new
|
|
15
22
|
catch :eof do
|
|
16
23
|
while true
|
|
24
|
+
guard_response_too_large!
|
|
17
25
|
read_line
|
|
18
|
-
|
|
26
|
+
# check before allocating memory for literal
|
|
27
|
+
guard_response_too_large!
|
|
28
|
+
break unless literal_size
|
|
19
29
|
read_literal
|
|
20
30
|
end
|
|
21
31
|
end
|
|
22
32
|
buff
|
|
23
33
|
ensure
|
|
24
|
-
@buff = nil
|
|
34
|
+
@buff = @literal_size = nil
|
|
25
35
|
end
|
|
26
36
|
|
|
27
37
|
private
|
|
28
38
|
|
|
39
|
+
# cached config
|
|
40
|
+
attr_reader :max_response_size
|
|
41
|
+
|
|
42
|
+
# response buffer state
|
|
29
43
|
attr_reader :buff, :literal_size
|
|
30
44
|
|
|
31
45
|
def bytes_read = buff.bytesize
|
|
32
46
|
def empty? = buff.empty?
|
|
33
|
-
def done? = line_done? && !
|
|
47
|
+
def done? = line_done? && !literal_size
|
|
34
48
|
def line_done? = buff.end_with?(CRLF)
|
|
35
|
-
|
|
49
|
+
|
|
50
|
+
def get_literal_size(buff)
|
|
51
|
+
buff.end_with?("}\r\n") && buff.rindex(/\{(\d+)\}\r\n\z/n) &&
|
|
52
|
+
coerce_number64($1)
|
|
53
|
+
rescue DataFormatError
|
|
54
|
+
raise DataFormatError, format("invalid response literal size (%s)", $1)
|
|
55
|
+
end
|
|
36
56
|
|
|
37
57
|
def read_line
|
|
38
|
-
|
|
39
|
-
|
|
58
|
+
line = (@sock.gets(CRLF, max_response_remaining) or throw :eof)
|
|
59
|
+
@literal_size = get_literal_size(line)
|
|
60
|
+
buff << line
|
|
40
61
|
end
|
|
41
62
|
|
|
42
63
|
def read_literal
|
|
43
|
-
# check before allocating memory for literal
|
|
44
|
-
max_response_remaining!
|
|
45
64
|
literal = String.new(capacity: literal_size)
|
|
46
|
-
buff << (@sock.read(
|
|
65
|
+
buff << (@sock.read(literal_size, literal) or throw :eof)
|
|
47
66
|
ensure
|
|
48
67
|
@literal_size = nil
|
|
49
68
|
end
|
|
50
69
|
|
|
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
70
|
def max_response_remaining = max_response_size &.- bytes_read
|
|
57
71
|
def response_too_large? = max_response_size &.< min_response_size
|
|
58
72
|
def min_response_size = bytes_read + min_response_remaining
|
|
@@ -61,8 +75,8 @@ module Net
|
|
|
61
75
|
empty? ? 3 : done? ? 0 : (literal_size || 0) + 2
|
|
62
76
|
end
|
|
63
77
|
|
|
64
|
-
def
|
|
65
|
-
return
|
|
78
|
+
def guard_response_too_large!
|
|
79
|
+
return unless response_too_large?
|
|
66
80
|
raise ResponseTooLargeError.new(
|
|
67
81
|
max_response_size:, bytes_read:, literal_size:,
|
|
68
82
|
)
|
|
@@ -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
|
-
|
|
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?
|