net-imap 0.3.7 → 0.5.6
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/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 +18 -6
- 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 -38
- 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
|