net-imap 0.4.24 → 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.
- checksums.yaml +4 -4
- data/Gemfile +8 -2
- data/lib/net/imap/authenticators.rb +2 -2
- data/lib/net/imap/command_data.rb +32 -182
- data/lib/net/imap/config/attr_type_coercion.rb +22 -23
- data/lib/net/imap/config.rb +38 -162
- data/lib/net/imap/data_encoding.rb +3 -3
- data/lib/net/imap/deprecated_client_options.rb +6 -3
- data/lib/net/imap/errors.rb +6 -33
- data/lib/net/imap/response_data.rb +62 -118
- data/lib/net/imap/response_parser.rb +18 -45
- data/lib/net/imap/sasl/authentication_exchange.rb +52 -20
- data/lib/net/imap/sasl/authenticators.rb +8 -4
- data/lib/net/imap/sasl/client_adapter.rb +77 -26
- data/lib/net/imap/sasl/cram_md5_authenticator.rb +1 -1
- data/lib/net/imap/sasl/digest_md5_authenticator.rb +213 -51
- data/lib/net/imap/sasl/login_authenticator.rb +2 -1
- data/lib/net/imap/sasl/protocol_adapters.rb +60 -4
- data/lib/net/imap/sasl/scram_authenticator.rb +0 -74
- data/lib/net/imap/sasl.rb +6 -3
- data/lib/net/imap/sasl_adapter.rb +0 -1
- data/lib/net/imap/sequence_set.rb +132 -282
- data/lib/net/imap.rb +97 -269
- data/net-imap.gemspec +1 -1
- metadata +7 -6
- data/lib/net/imap/response_reader.rb +0 -82
- data/lib/net/imap/uidplus_data.rb +0 -326
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Net::IMAP authenticator for the
|
|
3
|
+
# Net::IMAP authenticator for the +DIGEST-MD5+ SASL mechanism type, specified
|
|
4
4
|
# in RFC-2831[https://tools.ietf.org/html/rfc2831]. See Net::IMAP#authenticate.
|
|
5
5
|
#
|
|
6
6
|
# == Deprecated
|
|
@@ -9,11 +9,32 @@
|
|
|
9
9
|
# RFC-6331[https://tools.ietf.org/html/rfc6331] and should not be relied on for
|
|
10
10
|
# security. It is included for compatibility with existing servers.
|
|
11
11
|
class Net::IMAP::SASL::DigestMD5Authenticator
|
|
12
|
+
DataFormatError = Net::IMAP::DataFormatError
|
|
13
|
+
ResponseParseError = Net::IMAP::ResponseParseError
|
|
14
|
+
private_constant :DataFormatError, :ResponseParseError
|
|
15
|
+
|
|
12
16
|
STAGE_ONE = :stage_one
|
|
13
17
|
STAGE_TWO = :stage_two
|
|
14
18
|
STAGE_DONE = :stage_done
|
|
15
19
|
private_constant :STAGE_ONE, :STAGE_TWO, :STAGE_DONE
|
|
16
20
|
|
|
21
|
+
# Directives which must not have multiples. The RFC states:
|
|
22
|
+
# >>>
|
|
23
|
+
# This directive may appear at most once; if multiple instances are present,
|
|
24
|
+
# the client should abort the authentication exchange.
|
|
25
|
+
NO_MULTIPLES = %w[nonce stale maxbuf charset algorithm].freeze
|
|
26
|
+
|
|
27
|
+
# Required directives which must occur exactly once. The RFC states: >>>
|
|
28
|
+
# This directive is required and MUST appear exactly once; if not present,
|
|
29
|
+
# or if multiple instances are present, the client should abort the
|
|
30
|
+
# authentication exchange.
|
|
31
|
+
REQUIRED = %w[nonce algorithm].freeze
|
|
32
|
+
|
|
33
|
+
# Directives which are composed of one or more comma delimited tokens
|
|
34
|
+
QUOTED_LISTABLE = %w[qop cipher].freeze
|
|
35
|
+
|
|
36
|
+
private_constant :NO_MULTIPLES, :REQUIRED, :QUOTED_LISTABLE
|
|
37
|
+
|
|
17
38
|
# Authentication identity: the identity that matches the #password.
|
|
18
39
|
#
|
|
19
40
|
# RFC-2831[https://tools.ietf.org/html/rfc2831] uses the term +username+.
|
|
@@ -42,6 +63,59 @@ class Net::IMAP::SASL::DigestMD5Authenticator
|
|
|
42
63
|
#
|
|
43
64
|
attr_reader :authzid
|
|
44
65
|
|
|
66
|
+
# A namespace or collection of identities which contains +username+.
|
|
67
|
+
#
|
|
68
|
+
# Used by DIGEST-MD5, GSS-API, and NTLM. This is often a domain name that
|
|
69
|
+
# contains the name of the host performing the authentication.
|
|
70
|
+
#
|
|
71
|
+
# <em>Defaults to the last realm in the server-provided list of
|
|
72
|
+
# realms.</em>
|
|
73
|
+
attr_reader :realm
|
|
74
|
+
|
|
75
|
+
# Fully qualified canonical DNS host name for the requested service.
|
|
76
|
+
#
|
|
77
|
+
# <em>Defaults to #realm.</em>
|
|
78
|
+
attr_reader :host
|
|
79
|
+
|
|
80
|
+
# The service protocol, a
|
|
81
|
+
# {registered GSSAPI service name}[https://www.iana.org/assignments/gssapi-service-names/gssapi-service-names.xhtml],
|
|
82
|
+
# e.g. "imap", "ldap", or "xmpp".
|
|
83
|
+
#
|
|
84
|
+
# For Net::IMAP, the default is "imap" and should not be overridden. This
|
|
85
|
+
# must be set appropriately to use authenticators in other protocols.
|
|
86
|
+
#
|
|
87
|
+
# If an IANA-registered name isn't available, GSS-API
|
|
88
|
+
# (RFC-2743[https://tools.ietf.org/html/rfc2743]) allows the generic name
|
|
89
|
+
# "host".
|
|
90
|
+
attr_reader :service
|
|
91
|
+
|
|
92
|
+
# The generic server name when the server is replicated.
|
|
93
|
+
#
|
|
94
|
+
# +service_name+ will be ignored when it is +nil+ or identical to +host+.
|
|
95
|
+
#
|
|
96
|
+
# From RFC-2831[https://tools.ietf.org/html/rfc2831]:
|
|
97
|
+
# >>>
|
|
98
|
+
# The service is considered to be replicated if the client's
|
|
99
|
+
# service-location process involves resolution using standard DNS lookup
|
|
100
|
+
# operations, and if these operations involve DNS records (such as SRV, or
|
|
101
|
+
# MX) which resolve one DNS name into a set of other DNS names. In this
|
|
102
|
+
# case, the initial name used by the client is the "serv-name", and the
|
|
103
|
+
# final name is the "host" component.
|
|
104
|
+
attr_reader :service_name
|
|
105
|
+
|
|
106
|
+
# Parameters sent by the server are stored in this hash.
|
|
107
|
+
attr_reader :sparams
|
|
108
|
+
|
|
109
|
+
# The charset sent by the server. "UTF-8" (case insensitive) is the only
|
|
110
|
+
# allowed value. +nil+ should be interpreted as ISO 8859-1.
|
|
111
|
+
attr_reader :charset
|
|
112
|
+
|
|
113
|
+
# nonce sent by the server
|
|
114
|
+
attr_reader :nonce
|
|
115
|
+
|
|
116
|
+
# qop-options sent by the server
|
|
117
|
+
attr_reader :qop
|
|
118
|
+
|
|
45
119
|
# :call-seq:
|
|
46
120
|
# new(username, password, authzid = nil, **options) -> authenticator
|
|
47
121
|
# new(username:, password:, authzid: nil, **options) -> authenticator
|
|
@@ -64,27 +138,59 @@ class Net::IMAP::SASL::DigestMD5Authenticator
|
|
|
64
138
|
# When +authzid+ is not set, the server should derive the authorization
|
|
65
139
|
# identity from the authentication identity.
|
|
66
140
|
#
|
|
141
|
+
# * _optional_ #realm — A namespace for the #username, e.g. a domain.
|
|
142
|
+
# <em>Defaults to the last realm in the server-provided realms list.</em>
|
|
143
|
+
# * _optional_ #host — FQDN for requested service.
|
|
144
|
+
# <em>Defaults to</em> #realm.
|
|
145
|
+
# * _optional_ #service_name — The generic host name when the server is
|
|
146
|
+
# replicated.
|
|
147
|
+
# * _optional_ #service — the registered service protocol. E.g. "imap",
|
|
148
|
+
# "smtp", "ldap", "xmpp".
|
|
149
|
+
# <em>For Net::IMAP, this defaults to "imap".</em>
|
|
150
|
+
#
|
|
67
151
|
# * _optional_ +warn_deprecation+ — Set to +false+ to silence the warning.
|
|
68
152
|
#
|
|
69
153
|
# Any other keyword arguments are silently ignored.
|
|
70
154
|
def initialize(user = nil, pass = nil, authz = nil,
|
|
71
155
|
username: nil, password: nil, authzid: nil,
|
|
72
156
|
authcid: nil, secret: nil,
|
|
157
|
+
realm: nil, service: "imap", host: nil, service_name: nil,
|
|
73
158
|
warn_deprecation: true, **)
|
|
74
159
|
username = authcid || username || user or
|
|
75
160
|
raise ArgumentError, "missing username (authcid)"
|
|
76
161
|
password ||= secret || pass or raise ArgumentError, "missing password"
|
|
77
162
|
authzid ||= authz
|
|
78
163
|
if warn_deprecation
|
|
79
|
-
warn
|
|
80
|
-
|
|
164
|
+
warn("WARNING: DIGEST-MD5 SASL mechanism was deprecated by RFC6331.",
|
|
165
|
+
category: :deprecated)
|
|
81
166
|
end
|
|
167
|
+
|
|
82
168
|
require "digest/md5"
|
|
169
|
+
require "securerandom"
|
|
83
170
|
require "strscan"
|
|
84
171
|
@username, @password, @authzid = username, password, authzid
|
|
172
|
+
@realm = realm
|
|
173
|
+
@host = host
|
|
174
|
+
@service = service
|
|
175
|
+
@service_name = service_name
|
|
85
176
|
@nc, @stage = {}, STAGE_ONE
|
|
86
177
|
end
|
|
87
178
|
|
|
179
|
+
# From RFC-2831[https://tools.ietf.org/html/rfc2831]:
|
|
180
|
+
# >>>
|
|
181
|
+
# Indicates the principal name of the service with which the client wishes
|
|
182
|
+
# to connect, formed from the serv-type, host, and serv-name. For
|
|
183
|
+
# example, the FTP service on "ftp.example.com" would have a "digest-uri"
|
|
184
|
+
# value of "ftp/ftp.example.com"; the SMTP server from the example above
|
|
185
|
+
# would have a "digest-uri" value of "smtp/mail3.example.com/example.com".
|
|
186
|
+
def digest_uri
|
|
187
|
+
if service_name && service_name != host
|
|
188
|
+
"#{service}/#{host}/#{service_name}"
|
|
189
|
+
else
|
|
190
|
+
"#{service}/#{host}"
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
88
194
|
def initial_response?; false end
|
|
89
195
|
|
|
90
196
|
# Responds to server challenge in two stages.
|
|
@@ -92,78 +198,134 @@ class Net::IMAP::SASL::DigestMD5Authenticator
|
|
|
92
198
|
case @stage
|
|
93
199
|
when STAGE_ONE
|
|
94
200
|
@stage = STAGE_TWO
|
|
95
|
-
sparams =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if v =~ /,/
|
|
102
|
-
v = v.split(',')
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
sparams[k] = v
|
|
106
|
-
end
|
|
201
|
+
@sparams = parse_challenge(challenge)
|
|
202
|
+
@qop = sparams.key?("qop") ? ["auth"] : sparams["qop"].flatten
|
|
203
|
+
@nonce = sparams["nonce"] &.first
|
|
204
|
+
@charset = sparams["charset"]&.first
|
|
205
|
+
@realm ||= sparams["realm"] &.last
|
|
206
|
+
@host ||= realm
|
|
107
207
|
|
|
108
|
-
|
|
109
|
-
|
|
208
|
+
if !qop.include?("auth")
|
|
209
|
+
raise DataFormatError, "Server does not support auth (qop = %p)" % [
|
|
210
|
+
sparams["qop"]
|
|
211
|
+
]
|
|
212
|
+
elsif (emptykey = REQUIRED.find { sparams[_1].empty? })
|
|
213
|
+
raise DataFormatError, "Server didn't send %s (%p)" % [emptykey, challenge]
|
|
214
|
+
elsif (multikey = NO_MULTIPLES.find { sparams[_1].length > 1 })
|
|
215
|
+
raise DataFormatError, "Server sent multiple %s (%p)" % [multikey, challenge]
|
|
216
|
+
end
|
|
110
217
|
|
|
111
218
|
response = {
|
|
112
|
-
:nonce
|
|
113
|
-
:username
|
|
114
|
-
:realm
|
|
115
|
-
:
|
|
116
|
-
|
|
117
|
-
:
|
|
118
|
-
:
|
|
119
|
-
:
|
|
120
|
-
:charset
|
|
219
|
+
nonce: nonce,
|
|
220
|
+
username: username,
|
|
221
|
+
realm: realm,
|
|
222
|
+
cnonce: SecureRandom.base64(32),
|
|
223
|
+
"digest-uri": digest_uri,
|
|
224
|
+
qop: "auth",
|
|
225
|
+
maxbuf: 65535,
|
|
226
|
+
nc: "%08d" % nc(nonce),
|
|
227
|
+
charset: charset,
|
|
121
228
|
}
|
|
122
229
|
|
|
123
230
|
response[:authzid] = @authzid unless @authzid.nil?
|
|
124
231
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
a1 = [ a0, response.values_at(:nonce,:cnonce) ].join(':')
|
|
129
|
-
a1 << ':' + response[:authzid] unless response[:authzid].nil?
|
|
130
|
-
|
|
131
|
-
a2 = "AUTHENTICATE:" + response[:'digest-uri']
|
|
132
|
-
a2 << ":00000000000000000000000000000000" if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/
|
|
133
|
-
|
|
134
|
-
response[:response] = Digest::MD5.hexdigest(
|
|
135
|
-
[
|
|
136
|
-
Digest::MD5.hexdigest(a1),
|
|
137
|
-
response.values_at(:nonce, :nc, :cnonce, :qop),
|
|
138
|
-
Digest::MD5.hexdigest(a2)
|
|
139
|
-
].join(':')
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
return response.keys.map {|key| qdval(key.to_s, response[key]) }.join(',')
|
|
232
|
+
response[:response] = response_value(response)
|
|
233
|
+
format_response(response)
|
|
143
234
|
when STAGE_TWO
|
|
144
235
|
@stage = STAGE_DONE
|
|
145
|
-
|
|
146
|
-
if
|
|
147
|
-
return ''
|
|
148
|
-
else
|
|
149
|
-
raise ResponseParseError, challenge
|
|
150
|
-
end
|
|
236
|
+
raise ResponseParseError, challenge unless challenge =~ /rspauth=/
|
|
237
|
+
"" # if at the second stage, return an empty string
|
|
151
238
|
else
|
|
152
239
|
raise ResponseParseError, challenge
|
|
153
240
|
end
|
|
241
|
+
rescue => error
|
|
242
|
+
@stage = error
|
|
243
|
+
raise
|
|
154
244
|
end
|
|
155
245
|
|
|
156
246
|
def done?; @stage == STAGE_DONE end
|
|
157
247
|
|
|
158
248
|
private
|
|
159
249
|
|
|
250
|
+
LWS = /[\r\n \t]*/n # less strict than RFC, more strict than '\s'
|
|
251
|
+
TOKEN = /[^\x00-\x20\x7f()<>@,;:\\"\/\[\]?={}]+/n
|
|
252
|
+
QUOTED_STR = /"(?: [\t\x20-\x7e&&[^"]] | \\[\x00-\x7f] )*"/nx
|
|
253
|
+
LIST_DELIM = /(?:#{LWS} , )+ #{LWS}/nx
|
|
254
|
+
AUTH_PARAM = /
|
|
255
|
+
(#{TOKEN}) #{LWS} = #{LWS} (#{QUOTED_STR} | #{TOKEN}) #{LIST_DELIM}?
|
|
256
|
+
/nx
|
|
257
|
+
private_constant :LWS, :TOKEN, :QUOTED_STR, :LIST_DELIM, :AUTH_PARAM
|
|
258
|
+
|
|
259
|
+
def parse_challenge(challenge)
|
|
260
|
+
sparams = Hash.new {|h, k| h[k] = [] }
|
|
261
|
+
c = StringScanner.new(challenge)
|
|
262
|
+
c.skip LIST_DELIM
|
|
263
|
+
while c.scan AUTH_PARAM
|
|
264
|
+
k, v = c[1], c[2]
|
|
265
|
+
k = k.downcase
|
|
266
|
+
if v =~ /\A"(.*)"\z/mn
|
|
267
|
+
v = $1.gsub(/\\(.)/mn, '\1')
|
|
268
|
+
v = split_quoted_list(v, challenge) if QUOTED_LISTABLE.include? k
|
|
269
|
+
end
|
|
270
|
+
sparams[k] << v
|
|
271
|
+
end
|
|
272
|
+
if !c.eos?
|
|
273
|
+
raise DataFormatError, "Unparsable challenge: %p" % [challenge]
|
|
274
|
+
elsif sparams.empty?
|
|
275
|
+
raise DataFormatError, "Empty challenge: %p" % [challenge]
|
|
276
|
+
end
|
|
277
|
+
sparams
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def split_quoted_list(value, challenge)
|
|
281
|
+
value.split(LIST_DELIM).reject(&:empty?).tap do
|
|
282
|
+
_1.any? or raise DataFormatError, "Bad Challenge: %p" % [challenge]
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
160
286
|
def nc(nonce)
|
|
161
287
|
if @nc.has_key? nonce
|
|
162
288
|
@nc[nonce] = @nc[nonce] + 1
|
|
163
289
|
else
|
|
164
290
|
@nc[nonce] = 1
|
|
165
291
|
end
|
|
166
|
-
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def response_value(response)
|
|
295
|
+
a1 = compute_a1(response)
|
|
296
|
+
a2 = compute_a2(response)
|
|
297
|
+
Digest::MD5.hexdigest(
|
|
298
|
+
[
|
|
299
|
+
Digest::MD5.hexdigest(a1),
|
|
300
|
+
response.values_at(:nonce, :nc, :cnonce, :qop),
|
|
301
|
+
Digest::MD5.hexdigest(a2)
|
|
302
|
+
].join(":")
|
|
303
|
+
)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def compute_a0(response)
|
|
307
|
+
Digest::MD5.digest(
|
|
308
|
+
[ response.values_at(:username, :realm), password ].join(":")
|
|
309
|
+
)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def compute_a1(response)
|
|
313
|
+
a0 = compute_a0(response)
|
|
314
|
+
a1 = [ a0, response.values_at(:nonce, :cnonce) ].join(":")
|
|
315
|
+
a1 << ":#{response[:authzid]}" unless response[:authzid].nil?
|
|
316
|
+
a1
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def compute_a2(response)
|
|
320
|
+
a2 = "AUTHENTICATE:#{response[:"digest-uri"]}"
|
|
321
|
+
if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/
|
|
322
|
+
a2 << ":00000000000000000000000000000000"
|
|
323
|
+
end
|
|
324
|
+
a2
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def format_response(response)
|
|
328
|
+
response.map {|k, v| qdval(k.to_s, v) }.join(",")
|
|
167
329
|
end
|
|
168
330
|
|
|
169
331
|
# some responses need quoting
|
|
@@ -29,7 +29,8 @@ class Net::IMAP::SASL::LoginAuthenticator
|
|
|
29
29
|
warn_deprecation: true,
|
|
30
30
|
**)
|
|
31
31
|
if warn_deprecation
|
|
32
|
-
warn "WARNING: LOGIN SASL mechanism is deprecated. Use PLAIN instead."
|
|
32
|
+
warn "WARNING: LOGIN SASL mechanism is deprecated. Use PLAIN instead.",
|
|
33
|
+
category: :deprecated
|
|
33
34
|
end
|
|
34
35
|
@user = authcid || username || user
|
|
35
36
|
@password = password || secret || pass
|
|
@@ -4,16 +4,72 @@ module Net
|
|
|
4
4
|
class IMAP
|
|
5
5
|
module SASL
|
|
6
6
|
|
|
7
|
+
# SASL::ProtocolAdapters modules are meant to be used as mixins for
|
|
8
|
+
# SASL::ClientAdapter and its subclasses. Where the client adapter must
|
|
9
|
+
# be customized for each client library, the protocol adapter mixin
|
|
10
|
+
# handles \SASL requirements that are part of the protocol specification,
|
|
11
|
+
# but not specific to any particular client library. In particular, see
|
|
12
|
+
# {RFC4422 §4}[https://www.rfc-editor.org/rfc/rfc4422.html#section-4]
|
|
13
|
+
#
|
|
14
|
+
# === Interface
|
|
15
|
+
#
|
|
16
|
+
# >>>
|
|
17
|
+
# NOTE: This API is experimental, and may change.
|
|
18
|
+
#
|
|
19
|
+
# - {#command_name}[rdoc-ref:Generic#command_name] -- The name of the
|
|
20
|
+
# command used to to initiate an authentication exchange.
|
|
21
|
+
# - {#service}[rdoc-ref:Generic#service] -- The GSSAPI service name.
|
|
22
|
+
# - {#encode_ir}[rdoc-ref:Generic#encode_ir]--Encodes an initial response.
|
|
23
|
+
# - {#decode}[rdoc-ref:Generic#decode] -- Decodes a server challenge.
|
|
24
|
+
# - {#encode}[rdoc-ref:Generic#encode] -- Encodes a client response.
|
|
25
|
+
# - {#cancel_response}[rdoc-ref:Generic#cancel_response] -- The encoded
|
|
26
|
+
# client response used to cancel an authentication exchange.
|
|
27
|
+
#
|
|
28
|
+
# Other protocol requirements of the \SASL authentication exchange are
|
|
29
|
+
# handled by SASL::ClientAdapter.
|
|
30
|
+
#
|
|
31
|
+
# === Included protocol adapters
|
|
32
|
+
#
|
|
33
|
+
# - Generic -- a basic implementation of all of the methods listed above.
|
|
34
|
+
# - IMAP -- An adapter for the IMAP4 protocol.
|
|
35
|
+
# - SMTP -- An adapter for the \SMTP protocol with the +AUTH+ capability.
|
|
36
|
+
# - POP -- An adapter for the POP3 protocol with the +SASL+ capability.
|
|
7
37
|
module ProtocolAdapters
|
|
8
|
-
#
|
|
38
|
+
# See SASL::ProtocolAdapters@Interface.
|
|
9
39
|
module Generic
|
|
40
|
+
# The name of the protocol command used to initiate a \SASL
|
|
41
|
+
# authentication exchange.
|
|
42
|
+
#
|
|
43
|
+
# The generic implementation returns <tt>"AUTHENTICATE"</tt>.
|
|
10
44
|
def command_name; "AUTHENTICATE" end
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
45
|
+
|
|
46
|
+
# A service name from the {GSSAPI/Kerberos/SASL Service Names
|
|
47
|
+
# registry}[https://www.iana.org/assignments/gssapi-service-names/gssapi-service-names.xhtml].
|
|
48
|
+
#
|
|
49
|
+
# The generic implementation returns <tt>"host"</tt>, which is the
|
|
50
|
+
# generic GSSAPI host-based service name.
|
|
51
|
+
def service; "host" end
|
|
52
|
+
|
|
53
|
+
# Encodes an initial response string.
|
|
54
|
+
#
|
|
55
|
+
# The generic implementation returns the result of #encode, or returns
|
|
56
|
+
# <tt>"="</tt> when +string+ is empty.
|
|
14
57
|
def encode_ir(string) string.empty? ? "=" : encode(string) end
|
|
58
|
+
|
|
59
|
+
# Encodes a client response string.
|
|
60
|
+
#
|
|
61
|
+
# The generic implementation returns the Base64 encoding of +string+.
|
|
15
62
|
def encode(string) [string].pack("m0") end
|
|
63
|
+
|
|
64
|
+
# Decodes a server challenge string.
|
|
65
|
+
#
|
|
66
|
+
# The generic implementation returns the Base64 decoding of +string+.
|
|
16
67
|
def decode(string) string.unpack1("m0") end
|
|
68
|
+
|
|
69
|
+
# Returns the message used by the client to abort an authentication
|
|
70
|
+
# exchange.
|
|
71
|
+
#
|
|
72
|
+
# The generic implementation returns <tt>"*"</tt>.
|
|
17
73
|
def cancel_response; "*" end
|
|
18
74
|
end
|
|
19
75
|
|
|
@@ -75,19 +75,13 @@ 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).
|
|
79
78
|
#
|
|
80
79
|
# 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.
|
|
85
80
|
def initialize(username_arg = nil, password_arg = nil,
|
|
86
81
|
authcid: nil, username: nil,
|
|
87
82
|
authzid: nil,
|
|
88
83
|
password: nil, secret: nil,
|
|
89
84
|
min_iterations: 4096, # see both RFC5802 and RFC7677
|
|
90
|
-
max_iterations: 2**31 - 1, # max int32
|
|
91
85
|
cnonce: nil, # must only be set in tests
|
|
92
86
|
**options)
|
|
93
87
|
@username = username || username_arg || authcid or
|
|
@@ -100,22 +94,7 @@ module Net
|
|
|
100
94
|
@min_iterations.positive? or
|
|
101
95
|
raise ArgumentError, "min_iterations must be positive"
|
|
102
96
|
|
|
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
|
-
|
|
107
97
|
@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
|
|
119
98
|
end
|
|
120
99
|
|
|
121
100
|
# Authentication identity: the identity that matches the #password.
|
|
@@ -148,43 +127,8 @@ module Net
|
|
|
148
127
|
|
|
149
128
|
# The minimal allowed iteration count. Lower #iterations will raise an
|
|
150
129
|
# 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.
|
|
163
130
|
attr_reader :min_iterations
|
|
164
131
|
|
|
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
|
-
|
|
188
132
|
# The client nonce, generated by SecureRandom
|
|
189
133
|
attr_reader :cnonce
|
|
190
134
|
|
|
@@ -203,15 +147,6 @@ module Net
|
|
|
203
147
|
# Net::IMAP::NoResponseError.
|
|
204
148
|
attr_reader :server_error
|
|
205
149
|
|
|
206
|
-
# Memoized ScramAlgorithm#salted_password (needs #salt and #iterations)
|
|
207
|
-
def salted_password; @salted_password ||= compute_salted { super } end
|
|
208
|
-
|
|
209
|
-
# Memoized ScramAlgorithm#client_key (needs #salt and #iterations)
|
|
210
|
-
def client_key; @client_key ||= compute_salted { super } end
|
|
211
|
-
|
|
212
|
-
# Memoized ScramAlgorithm#server_key (needs #salt and #iterations)
|
|
213
|
-
def server_key; @server_key ||= compute_salted { super } end
|
|
214
|
-
|
|
215
150
|
# Returns a new OpenSSL::Digest object, set to the appropriate hash
|
|
216
151
|
# function for the chosen mechanism.
|
|
217
152
|
#
|
|
@@ -251,13 +186,6 @@ module Net
|
|
|
251
186
|
|
|
252
187
|
private
|
|
253
188
|
|
|
254
|
-
# Checks for +salt+ and +iterations+ before yielding
|
|
255
|
-
def compute_salted
|
|
256
|
-
String === salt or raise Error, "unknown salt"
|
|
257
|
-
Integer === iterations or raise Error, "unknown iterations"
|
|
258
|
-
yield
|
|
259
|
-
end
|
|
260
|
-
|
|
261
189
|
# Need to store this for auth_message
|
|
262
190
|
attr_reader :server_first_message
|
|
263
191
|
|
|
@@ -274,8 +202,6 @@ module Net
|
|
|
274
202
|
raise Error, "server did not send iteration count"
|
|
275
203
|
min_iterations <= iterations or
|
|
276
204
|
raise Error, "too few iterations: %d" % [iterations]
|
|
277
|
-
max_iterations.nil? || iterations <= max_iterations or
|
|
278
|
-
raise Error, "too many iterations: %d" % [iterations]
|
|
279
205
|
mext = sparams["m"] and
|
|
280
206
|
raise Error, "mandatory extension: %p" % [mext]
|
|
281
207
|
snonce.start_with? cnonce or
|
data/lib/net/imap/sasl.rb
CHANGED
|
@@ -114,8 +114,8 @@ module Net
|
|
|
114
114
|
# messages has not passed integrity checks.
|
|
115
115
|
AuthenticationFailed = Class.new(Error)
|
|
116
116
|
|
|
117
|
-
# Indicates that authentication cannot proceed because
|
|
118
|
-
#
|
|
117
|
+
# Indicates that authentication cannot proceed because the server ended
|
|
118
|
+
# authentication prematurely.
|
|
119
119
|
class AuthenticationIncomplete < AuthenticationFailed
|
|
120
120
|
# The success response from the server
|
|
121
121
|
attr_reader :response
|
|
@@ -159,7 +159,10 @@ module Net
|
|
|
159
159
|
# Returns the default global SASL::Authenticators instance.
|
|
160
160
|
def self.authenticators; @authenticators ||= Authenticators.new end
|
|
161
161
|
|
|
162
|
-
#
|
|
162
|
+
# Creates a new SASL authenticator, using SASL::Authenticators#new.
|
|
163
|
+
#
|
|
164
|
+
# +registry+ defaults to SASL.authenticators. All other arguments are
|
|
165
|
+
# forwarded to to <tt>registry.new</tt>.
|
|
163
166
|
def self.authenticator(*args, registry: authenticators, **kwargs, &block)
|
|
164
167
|
registry.new(*args, **kwargs, &block)
|
|
165
168
|
end
|
|
@@ -12,7 +12,6 @@ module Net
|
|
|
12
12
|
|
|
13
13
|
def response_errors; RESPONSE_ERRORS end
|
|
14
14
|
def sasl_ir_capable?; client.capable?("SASL-IR") end
|
|
15
|
-
def auth_capable?(mechanism); client.auth_capable?(mechanism) end
|
|
16
15
|
def drop_connection; client.logout! end
|
|
17
16
|
def drop_connection!; client.disconnect end
|
|
18
17
|
end
|