net-imap 0.3.4 → 0.5.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/BSDL +22 -0
- data/COPYING +56 -0
- data/Gemfile +14 -0
- data/LICENSE.txt +3 -22
- data/README.md +25 -8
- data/Rakefile +0 -7
- data/docs/styles.css +72 -23
- data/lib/net/imap/authenticators.rb +26 -57
- data/lib/net/imap/command_data.rb +74 -54
- data/lib/net/imap/config/attr_accessors.rb +75 -0
- data/lib/net/imap/config/attr_inheritance.rb +90 -0
- data/lib/net/imap/config/attr_type_coercion.rb +61 -0
- data/lib/net/imap/config.rb +470 -0
- data/lib/net/imap/data_encoding.rb +21 -9
- data/lib/net/imap/data_lite.rb +226 -0
- data/lib/net/imap/deprecated_client_options.rb +142 -0
- data/lib/net/imap/errors.rb +27 -1
- data/lib/net/imap/esearch_result.rb +180 -0
- data/lib/net/imap/fetch_data.rb +597 -0
- data/lib/net/imap/flags.rb +1 -1
- data/lib/net/imap/response_data.rb +250 -440
- data/lib/net/imap/response_parser/parser_utils.rb +245 -0
- data/lib/net/imap/response_parser.rb +1867 -1184
- data/lib/net/imap/sasl/anonymous_authenticator.rb +69 -0
- data/lib/net/imap/sasl/authentication_exchange.rb +139 -0
- data/lib/net/imap/sasl/authenticators.rb +122 -0
- data/lib/net/imap/sasl/client_adapter.rb +123 -0
- data/lib/net/imap/{authenticators/cram_md5.rb → sasl/cram_md5_authenticator.rb} +24 -14
- data/lib/net/imap/sasl/digest_md5_authenticator.rb +342 -0
- data/lib/net/imap/sasl/external_authenticator.rb +83 -0
- data/lib/net/imap/sasl/gs2_header.rb +80 -0
- data/lib/net/imap/{authenticators/login.rb → sasl/login_authenticator.rb} +28 -18
- data/lib/net/imap/sasl/oauthbearer_authenticator.rb +199 -0
- data/lib/net/imap/sasl/plain_authenticator.rb +101 -0
- data/lib/net/imap/sasl/protocol_adapters.rb +101 -0
- data/lib/net/imap/sasl/scram_algorithm.rb +58 -0
- data/lib/net/imap/sasl/scram_authenticator.rb +287 -0
- data/lib/net/imap/sasl/stringprep.rb +6 -66
- data/lib/net/imap/sasl/xoauth2_authenticator.rb +106 -0
- data/lib/net/imap/sasl.rb +148 -44
- data/lib/net/imap/sasl_adapter.rb +20 -0
- data/lib/net/imap/search_result.rb +146 -0
- data/lib/net/imap/sequence_set.rb +1565 -0
- data/lib/net/imap/stringprep/nameprep.rb +70 -0
- data/lib/net/imap/stringprep/saslprep.rb +69 -0
- data/lib/net/imap/stringprep/saslprep_tables.rb +96 -0
- data/lib/net/imap/stringprep/tables.rb +146 -0
- data/lib/net/imap/stringprep/trace.rb +85 -0
- data/lib/net/imap/stringprep.rb +159 -0
- data/lib/net/imap/uidplus_data.rb +244 -0
- data/lib/net/imap/vanished_data.rb +56 -0
- data/lib/net/imap.rb +2090 -823
- data/net-imap.gemspec +7 -8
- data/rakelib/benchmarks.rake +91 -0
- data/rakelib/rfcs.rake +2 -0
- data/rakelib/saslprep.rake +4 -4
- data/rakelib/string_prep_tables_generator.rb +84 -60
- data/sample/net-imap.rb +167 -0
- metadata +45 -49
- data/.github/dependabot.yml +0 -6
- data/.github/workflows/test.yml +0 -31
- data/.gitignore +0 -10
- data/benchmarks/stringprep.yml +0 -65
- data/benchmarks/table-regexps.yml +0 -39
- data/lib/net/imap/authenticators/digest_md5.rb +0 -115
- data/lib/net/imap/authenticators/plain.rb +0 -41
- data/lib/net/imap/authenticators/xoauth2.rb +0 -20
- data/lib/net/imap/sasl/saslprep.rb +0 -55
- data/lib/net/imap/sasl/saslprep_tables.rb +0 -98
- data/lib/net/imap/sasl/stringprep_tables.rb +0 -153
@@ -0,0 +1,342 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Net::IMAP authenticator for the +DIGEST-MD5+ SASL mechanism type, specified
|
4
|
+
# in RFC-2831[https://www.rfc-editor.org/rfc/rfc2831]. See Net::IMAP#authenticate.
|
5
|
+
#
|
6
|
+
# == Deprecated
|
7
|
+
#
|
8
|
+
# "+DIGEST-MD5+" has been deprecated by
|
9
|
+
# RFC-6331[https://www.rfc-editor.org/rfc/rfc6331] and should not be relied on for
|
10
|
+
# security. It is included for compatibility with existing servers.
|
11
|
+
class Net::IMAP::SASL::DigestMD5Authenticator
|
12
|
+
DataFormatError = Net::IMAP::DataFormatError
|
13
|
+
ResponseParseError = Net::IMAP::ResponseParseError
|
14
|
+
private_constant :DataFormatError, :ResponseParseError
|
15
|
+
|
16
|
+
STAGE_ONE = :stage_one
|
17
|
+
STAGE_TWO = :stage_two
|
18
|
+
STAGE_DONE = :stage_done
|
19
|
+
private_constant :STAGE_ONE, :STAGE_TWO, :STAGE_DONE
|
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
|
+
|
38
|
+
# Authentication identity: the identity that matches the #password.
|
39
|
+
#
|
40
|
+
# RFC-2831[https://www.rfc-editor.org/rfc/rfc2831] uses the term +username+.
|
41
|
+
# "Authentication identity" is the generic term used by
|
42
|
+
# RFC-4422[https://www.rfc-editor.org/rfc/rfc4422].
|
43
|
+
# RFC-4616[https://www.rfc-editor.org/rfc/rfc4616] and many later RFCs abbreviate
|
44
|
+
# this to +authcid+.
|
45
|
+
attr_reader :username
|
46
|
+
alias authcid username
|
47
|
+
|
48
|
+
# A password or passphrase that matches the #username.
|
49
|
+
#
|
50
|
+
# The +password+ will be used to create the response digest.
|
51
|
+
attr_reader :password
|
52
|
+
|
53
|
+
# Authorization identity: an identity to act as or on behalf of. The identity
|
54
|
+
# form is application protocol specific. If not provided or left blank, the
|
55
|
+
# server derives an authorization identity from the authentication identity.
|
56
|
+
# The server is responsible for verifying the client's credentials and
|
57
|
+
# verifying that the identity it associates with the client's authentication
|
58
|
+
# identity is allowed to act as (or on behalf of) the authorization identity.
|
59
|
+
#
|
60
|
+
# For example, an administrator or superuser might take on another role:
|
61
|
+
#
|
62
|
+
# imap.authenticate "DIGEST-MD5", "root", ->{passwd}, authzid: "user"
|
63
|
+
#
|
64
|
+
attr_reader :authzid
|
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://www.rfc-editor.org/rfc/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://www.rfc-editor.org/rfc/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
|
+
|
119
|
+
# :call-seq:
|
120
|
+
# new(username, password, authzid = nil, **options) -> authenticator
|
121
|
+
# new(username:, password:, authzid: nil, **options) -> authenticator
|
122
|
+
# new(authcid:, password:, authzid: nil, **options) -> authenticator
|
123
|
+
#
|
124
|
+
# Creates an Authenticator for the "+DIGEST-MD5+" SASL mechanism.
|
125
|
+
#
|
126
|
+
# Called by Net::IMAP#authenticate and similar methods on other clients.
|
127
|
+
#
|
128
|
+
# ==== Parameters
|
129
|
+
#
|
130
|
+
# * #authcid ― Authentication identity that is associated with #password.
|
131
|
+
#
|
132
|
+
# #username ― An alias for +authcid+.
|
133
|
+
#
|
134
|
+
# * #password ― A password or passphrase associated with this #authcid.
|
135
|
+
#
|
136
|
+
# * _optional_ #authzid ― Authorization identity to act as or on behalf of.
|
137
|
+
#
|
138
|
+
# When +authzid+ is not set, the server should derive the authorization
|
139
|
+
# identity from the authentication identity.
|
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
|
+
#
|
151
|
+
# * _optional_ +warn_deprecation+ — Set to +false+ to silence the warning.
|
152
|
+
#
|
153
|
+
# Any other keyword arguments are silently ignored.
|
154
|
+
def initialize(user = nil, pass = nil, authz = nil,
|
155
|
+
username: nil, password: nil, authzid: nil,
|
156
|
+
authcid: nil, secret: nil,
|
157
|
+
realm: nil, service: "imap", host: nil, service_name: nil,
|
158
|
+
warn_deprecation: true, **)
|
159
|
+
username = authcid || username || user or
|
160
|
+
raise ArgumentError, "missing username (authcid)"
|
161
|
+
password ||= secret || pass or raise ArgumentError, "missing password"
|
162
|
+
authzid ||= authz
|
163
|
+
if warn_deprecation
|
164
|
+
warn("WARNING: DIGEST-MD5 SASL mechanism was deprecated by RFC6331.",
|
165
|
+
category: :deprecated)
|
166
|
+
end
|
167
|
+
|
168
|
+
require "digest/md5"
|
169
|
+
require "securerandom"
|
170
|
+
require "strscan"
|
171
|
+
@username, @password, @authzid = username, password, authzid
|
172
|
+
@realm = realm
|
173
|
+
@host = host
|
174
|
+
@service = service
|
175
|
+
@service_name = service_name
|
176
|
+
@nc, @stage = {}, STAGE_ONE
|
177
|
+
end
|
178
|
+
|
179
|
+
# From RFC-2831[https://www.rfc-editor.org/rfc/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
|
+
|
194
|
+
def initial_response?; false end
|
195
|
+
|
196
|
+
# Responds to server challenge in two stages.
|
197
|
+
def process(challenge)
|
198
|
+
case @stage
|
199
|
+
when STAGE_ONE
|
200
|
+
@stage = STAGE_TWO
|
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
|
207
|
+
|
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
|
217
|
+
|
218
|
+
response = {
|
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,
|
228
|
+
}
|
229
|
+
|
230
|
+
response[:authzid] = @authzid unless @authzid.nil?
|
231
|
+
|
232
|
+
response[:response] = response_value(response)
|
233
|
+
format_response(response)
|
234
|
+
when STAGE_TWO
|
235
|
+
@stage = STAGE_DONE
|
236
|
+
raise ResponseParseError, challenge unless challenge =~ /rspauth=/
|
237
|
+
"" # if at the second stage, return an empty string
|
238
|
+
else
|
239
|
+
raise ResponseParseError, challenge
|
240
|
+
end
|
241
|
+
rescue => error
|
242
|
+
@stage = error
|
243
|
+
raise
|
244
|
+
end
|
245
|
+
|
246
|
+
def done?; @stage == STAGE_DONE end
|
247
|
+
|
248
|
+
private
|
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
|
+
|
286
|
+
def nc(nonce)
|
287
|
+
if @nc.has_key? nonce
|
288
|
+
@nc[nonce] = @nc[nonce] + 1
|
289
|
+
else
|
290
|
+
@nc[nonce] = 1
|
291
|
+
end
|
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(",")
|
329
|
+
end
|
330
|
+
|
331
|
+
# some responses need quoting
|
332
|
+
def qdval(k, v)
|
333
|
+
return if k.nil? or v.nil?
|
334
|
+
if %w"username authzid realm nonce cnonce digest-uri qop".include? k
|
335
|
+
v = v.gsub(/([\\"])/, "\\\1")
|
336
|
+
return '%s="%s"' % [k, v]
|
337
|
+
else
|
338
|
+
return '%s=%s' % [k, v]
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Net
|
4
|
+
class IMAP < Protocol
|
5
|
+
module SASL
|
6
|
+
|
7
|
+
# Authenticator for the "+EXTERNAL+" SASL mechanism, as specified by
|
8
|
+
# RFC-4422[https://www.rfc-editor.org/rfc/rfc4422]. See
|
9
|
+
# Net::IMAP#authenticate.
|
10
|
+
#
|
11
|
+
# The EXTERNAL mechanism requests that the server use client credentials
|
12
|
+
# established external to SASL, for example by TLS certificate or IPSec.
|
13
|
+
class ExternalAuthenticator
|
14
|
+
|
15
|
+
# Authorization identity: an identity to act as or on behalf of. The
|
16
|
+
# identity form is application protocol specific. If not provided or
|
17
|
+
# left blank, the server derives an authorization identity from the
|
18
|
+
# authentication identity. The server is responsible for verifying the
|
19
|
+
# client's credentials and verifying that the identity it associates
|
20
|
+
# with the client's authentication identity is allowed to act as (or on
|
21
|
+
# behalf of) the authorization identity.
|
22
|
+
#
|
23
|
+
# For example, an administrator or superuser might take on another role:
|
24
|
+
#
|
25
|
+
# imap.authenticate "PLAIN", "root", passwd, authzid: "user"
|
26
|
+
#
|
27
|
+
attr_reader :authzid
|
28
|
+
alias username authzid
|
29
|
+
|
30
|
+
# :call-seq:
|
31
|
+
# new(authzid: nil, **) -> authenticator
|
32
|
+
# new(username: nil, **) -> authenticator
|
33
|
+
# new(username = nil, **) -> authenticator
|
34
|
+
#
|
35
|
+
# Creates an Authenticator for the "+EXTERNAL+" SASL mechanism, as
|
36
|
+
# specified in RFC-4422[https://www.rfc-editor.org/rfc/rfc4422]. To use
|
37
|
+
# this, see Net::IMAP#authenticate or your client's authentication
|
38
|
+
# method.
|
39
|
+
#
|
40
|
+
# ==== Parameters
|
41
|
+
#
|
42
|
+
# * _optional_ #authzid ― Authorization identity to act as or on behalf of.
|
43
|
+
#
|
44
|
+
# _optional_ #username ― An alias for #authzid.
|
45
|
+
#
|
46
|
+
# Note that, unlike some other authenticators, +username+ sets the
|
47
|
+
# _authorization_ identity and not the _authentication_ identity. The
|
48
|
+
# authentication identity is established for the client by the
|
49
|
+
# external credentials.
|
50
|
+
#
|
51
|
+
# Any other keyword parameters are quietly ignored.
|
52
|
+
def initialize(user = nil, authzid: nil, username: nil, **)
|
53
|
+
authzid ||= username || user
|
54
|
+
@authzid = authzid&.to_str&.encode "UTF-8"
|
55
|
+
if @authzid&.match?(/\u0000/u) # also validates UTF8 encoding
|
56
|
+
raise ArgumentError, "contains NULL"
|
57
|
+
end
|
58
|
+
@done = false
|
59
|
+
end
|
60
|
+
|
61
|
+
# :call-seq:
|
62
|
+
# initial_response? -> true
|
63
|
+
#
|
64
|
+
# +EXTERNAL+ can send an initial client response.
|
65
|
+
def initial_response?; true end
|
66
|
+
|
67
|
+
# Returns #authzid, or an empty string if there is no authzid.
|
68
|
+
def process(_)
|
69
|
+
authzid || ""
|
70
|
+
ensure
|
71
|
+
@done = true
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns true when the initial client response was sent.
|
75
|
+
#
|
76
|
+
# The authentication should not succeed unless this returns true, but it
|
77
|
+
# does *not* indicate success.
|
78
|
+
def done?; @done end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Net
|
4
|
+
class IMAP < Protocol
|
5
|
+
module SASL
|
6
|
+
|
7
|
+
# Originally defined for the GS2 mechanism family in
|
8
|
+
# RFC5801[https://www.rfc-editor.org/rfc/rfc5801],
|
9
|
+
# several different mechanisms start with a GS2 header:
|
10
|
+
# * +GS2-*+ --- RFC5801[https://www.rfc-editor.org/rfc/rfc5801]
|
11
|
+
# * +SCRAM-*+ --- RFC5802[https://www.rfc-editor.org/rfc/rfc5802]
|
12
|
+
# (ScramAuthenticator)
|
13
|
+
# * +SAML20+ --- RFC6595[https://www.rfc-editor.org/rfc/rfc6595]
|
14
|
+
# * +OPENID20+ --- RFC6616[https://www.rfc-editor.org/rfc/rfc6616]
|
15
|
+
# * +OAUTH10A+ --- RFC7628[https://www.rfc-editor.org/rfc/rfc7628]
|
16
|
+
# * +OAUTHBEARER+ --- RFC7628[https://www.rfc-editor.org/rfc/rfc7628]
|
17
|
+
# (OAuthBearerAuthenticator)
|
18
|
+
#
|
19
|
+
# Classes that include this module must implement +#authzid+.
|
20
|
+
module GS2Header
|
21
|
+
NO_NULL_CHARS = /\A[^\x00]+\z/u.freeze # :nodoc:
|
22
|
+
|
23
|
+
##
|
24
|
+
# Matches {RFC5801 §4}[https://www.rfc-editor.org/rfc/rfc5801#section-4]
|
25
|
+
# +saslname+. The output from gs2_saslname_encode matches this Regexp.
|
26
|
+
RFC5801_SASLNAME = /\A(?:[^,=\x00]|=2C|=3D)+\z/u.freeze
|
27
|
+
|
28
|
+
# The {RFC5801 §4}[https://www.rfc-editor.org/rfc/rfc5801#section-4]
|
29
|
+
# +gs2-header+, which prefixes the #initial_client_response.
|
30
|
+
#
|
31
|
+
# >>>
|
32
|
+
# <em>Note: the actual GS2 header includes an optional flag to
|
33
|
+
# indicate that the GSS mechanism is not "standard", but since all of
|
34
|
+
# the SASL mechanisms using GS2 are "standard", we don't include that
|
35
|
+
# flag. A class for a nonstandard GSSAPI mechanism should prefix with
|
36
|
+
# "+F,+".</em>
|
37
|
+
def gs2_header
|
38
|
+
"#{gs2_cb_flag},#{gs2_authzid},"
|
39
|
+
end
|
40
|
+
|
41
|
+
# The {RFC5801 §4}[https://www.rfc-editor.org/rfc/rfc5801#section-4]
|
42
|
+
# +gs2-cb-flag+:
|
43
|
+
#
|
44
|
+
# "+n+":: The client doesn't support channel binding.
|
45
|
+
# "+y+":: The client does support channel binding
|
46
|
+
# but thinks the server does not.
|
47
|
+
# "+p+":: The client requires channel binding.
|
48
|
+
# The selected channel binding follows "+p=+".
|
49
|
+
#
|
50
|
+
# The default always returns "+n+". A mechanism that supports channel
|
51
|
+
# binding must override this method.
|
52
|
+
#
|
53
|
+
def gs2_cb_flag; "n" end
|
54
|
+
|
55
|
+
# The {RFC5801 §4}[https://www.rfc-editor.org/rfc/rfc5801#section-4]
|
56
|
+
# +gs2-authzid+ header, when +#authzid+ is not empty.
|
57
|
+
#
|
58
|
+
# If +#authzid+ is empty or +nil+, an empty string is returned.
|
59
|
+
def gs2_authzid
|
60
|
+
return "" if authzid.nil? || authzid == ""
|
61
|
+
"a=#{gs2_saslname_encode(authzid)}"
|
62
|
+
end
|
63
|
+
|
64
|
+
module_function
|
65
|
+
|
66
|
+
# Encodes +str+ to match RFC5801_SASLNAME.
|
67
|
+
def gs2_saslname_encode(str)
|
68
|
+
str = str.encode("UTF-8")
|
69
|
+
# Regexp#match raises "invalid byte sequence" for invalid UTF-8
|
70
|
+
NO_NULL_CHARS.match str or
|
71
|
+
raise ArgumentError, "invalid saslname: %p" % [str]
|
72
|
+
str
|
73
|
+
.gsub(?=, "=3D")
|
74
|
+
.gsub(?,, "=2C")
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -3,9 +3,9 @@
|
|
3
3
|
# Authenticator for the "+LOGIN+" SASL mechanism. See Net::IMAP#authenticate.
|
4
4
|
#
|
5
5
|
# +LOGIN+ authentication sends the password in cleartext.
|
6
|
-
# RFC3501[https://
|
6
|
+
# RFC3501[https://www.rfc-editor.org/rfc/rfc3501] encourages servers to disable
|
7
7
|
# cleartext authentication until after TLS has been negotiated.
|
8
|
-
# RFC8314[https://
|
8
|
+
# RFC8314[https://www.rfc-editor.org/rfc/rfc8314] recommends TLS version 1.2 or
|
9
9
|
# greater be used for all traffic, and deprecate cleartext access ASAP. +LOGIN+
|
10
10
|
# can be secured by TLS encryption.
|
11
11
|
#
|
@@ -17,30 +17,40 @@
|
|
17
17
|
# compatibility with existing servers. See
|
18
18
|
# {draft-murchison-sasl-login}[https://www.iana.org/go/draft-murchison-sasl-login]
|
19
19
|
# for both specification and deprecation.
|
20
|
-
class Net::IMAP::LoginAuthenticator
|
20
|
+
class Net::IMAP::SASL::LoginAuthenticator
|
21
|
+
STATE_USER = :USER
|
22
|
+
STATE_PASSWORD = :PASSWORD
|
23
|
+
STATE_DONE = :DONE
|
24
|
+
private_constant :STATE_USER, :STATE_PASSWORD, :STATE_DONE
|
25
|
+
|
26
|
+
def initialize(user = nil, pass = nil,
|
27
|
+
authcid: nil, username: nil,
|
28
|
+
password: nil, secret: nil,
|
29
|
+
warn_deprecation: true,
|
30
|
+
**)
|
31
|
+
if warn_deprecation
|
32
|
+
warn "WARNING: LOGIN SASL mechanism is deprecated. Use PLAIN instead.",
|
33
|
+
category: :deprecated
|
34
|
+
end
|
35
|
+
@user = authcid || username || user
|
36
|
+
@password = password || secret || pass
|
37
|
+
@state = STATE_USER
|
38
|
+
end
|
39
|
+
|
40
|
+
def initial_response?; false end
|
41
|
+
|
21
42
|
def process(data)
|
22
43
|
case @state
|
23
44
|
when STATE_USER
|
24
45
|
@state = STATE_PASSWORD
|
25
46
|
return @user
|
26
47
|
when STATE_PASSWORD
|
48
|
+
@state = STATE_DONE
|
27
49
|
return @password
|
50
|
+
when STATE_DONE
|
51
|
+
raise ResponseParseError, data
|
28
52
|
end
|
29
53
|
end
|
30
54
|
|
31
|
-
|
32
|
-
|
33
|
-
STATE_USER = :USER
|
34
|
-
STATE_PASSWORD = :PASSWORD
|
35
|
-
|
36
|
-
def initialize(user, password, warn_deprecation: true, **_ignored)
|
37
|
-
if warn_deprecation
|
38
|
-
warn "WARNING: LOGIN SASL mechanism is deprecated. Use PLAIN instead."
|
39
|
-
end
|
40
|
-
@user = user
|
41
|
-
@password = password
|
42
|
-
@state = STATE_USER
|
43
|
-
end
|
44
|
-
|
45
|
-
Net::IMAP.add_authenticator "LOGIN", self
|
55
|
+
def done?; @state == STATE_DONE end
|
46
56
|
end
|