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.
- 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 +170 -12
- data/lib/net/imap/config.rb +34 -0
- data/lib/net/imap/data_encoding.rb +50 -0
- data/lib/net/imap/errors.rb +47 -10
- data/lib/net/imap/response_data.rb +108 -11
- data/lib/net/imap/response_parser.rb +13 -0
- data/lib/net/imap/response_reader.rb +25 -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 +167 -61
- data/rakelib/rdoc.rake +1 -18
- metadata +4 -2
|
@@ -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
|
-
|
|
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? && !
|
|
45
|
+
def done? = line_done? && !literal_size
|
|
34
46
|
def line_done? = buff.end_with?(CRLF)
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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(
|
|
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
|
|
65
|
-
return
|
|
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
|
-
|
|
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?
|