net-imap 0.3.4 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of net-imap might be problematic. Click here for more details.

Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/pages.yml +46 -0
  3. data/.github/workflows/test.yml +12 -12
  4. data/Gemfile +1 -0
  5. data/README.md +15 -4
  6. data/Rakefile +0 -7
  7. data/benchmarks/generate_parser_benchmarks +52 -0
  8. data/benchmarks/parser.yml +578 -0
  9. data/benchmarks/stringprep.yml +1 -1
  10. data/lib/net/imap/authenticators.rb +26 -57
  11. data/lib/net/imap/command_data.rb +13 -6
  12. data/lib/net/imap/data_encoding.rb +3 -3
  13. data/lib/net/imap/deprecated_client_options.rb +139 -0
  14. data/lib/net/imap/response_data.rb +46 -41
  15. data/lib/net/imap/response_parser/parser_utils.rb +230 -0
  16. data/lib/net/imap/response_parser.rb +665 -627
  17. data/lib/net/imap/sasl/anonymous_authenticator.rb +68 -0
  18. data/lib/net/imap/sasl/authentication_exchange.rb +107 -0
  19. data/lib/net/imap/sasl/authenticators.rb +118 -0
  20. data/lib/net/imap/sasl/client_adapter.rb +72 -0
  21. data/lib/net/imap/{authenticators/cram_md5.rb → sasl/cram_md5_authenticator.rb} +15 -9
  22. data/lib/net/imap/sasl/digest_md5_authenticator.rb +168 -0
  23. data/lib/net/imap/sasl/external_authenticator.rb +62 -0
  24. data/lib/net/imap/sasl/gs2_header.rb +80 -0
  25. data/lib/net/imap/{authenticators/login.rb → sasl/login_authenticator.rb} +19 -14
  26. data/lib/net/imap/sasl/oauthbearer_authenticator.rb +164 -0
  27. data/lib/net/imap/sasl/plain_authenticator.rb +93 -0
  28. data/lib/net/imap/sasl/protocol_adapters.rb +45 -0
  29. data/lib/net/imap/sasl/scram_algorithm.rb +58 -0
  30. data/lib/net/imap/sasl/scram_authenticator.rb +278 -0
  31. data/lib/net/imap/sasl/stringprep.rb +6 -66
  32. data/lib/net/imap/sasl/xoauth2_authenticator.rb +88 -0
  33. data/lib/net/imap/sasl.rb +144 -43
  34. data/lib/net/imap/sasl_adapter.rb +21 -0
  35. data/lib/net/imap/stringprep/nameprep.rb +70 -0
  36. data/lib/net/imap/stringprep/saslprep.rb +69 -0
  37. data/lib/net/imap/stringprep/saslprep_tables.rb +96 -0
  38. data/lib/net/imap/stringprep/tables.rb +146 -0
  39. data/lib/net/imap/stringprep/trace.rb +85 -0
  40. data/lib/net/imap/stringprep.rb +159 -0
  41. data/lib/net/imap.rb +976 -590
  42. data/net-imap.gemspec +2 -2
  43. data/rakelib/saslprep.rake +4 -4
  44. data/rakelib/string_prep_tables_generator.rb +82 -60
  45. metadata +31 -12
  46. data/lib/net/imap/authenticators/digest_md5.rb +0 -115
  47. data/lib/net/imap/authenticators/plain.rb +0 -41
  48. data/lib/net/imap/authenticators/xoauth2.rb +0 -20
  49. data/lib/net/imap/sasl/saslprep.rb +0 -55
  50. data/lib/net/imap/sasl/saslprep_tables.rb +0 -98
  51. data/lib/net/imap/sasl/stringprep_tables.rb +0 -153
@@ -0,0 +1,278 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "securerandom"
5
+
6
+ require_relative "gs2_header"
7
+ require_relative "scram_algorithm"
8
+
9
+ module Net
10
+ class IMAP
11
+ module SASL
12
+
13
+ # Abstract base class for the "+SCRAM-*+" family of SASL mechanisms,
14
+ # defined in RFC5802[https://tools.ietf.org/html/rfc5802]. Use via
15
+ # Net::IMAP#authenticate.
16
+ #
17
+ # Directly supported:
18
+ # * +SCRAM-SHA-1+ --- ScramSHA1Authenticator
19
+ # * +SCRAM-SHA-256+ --- ScramSHA256Authenticator
20
+ #
21
+ # New +SCRAM-*+ mechanisms can easily be added for any hash algorithm
22
+ # supported by
23
+ # OpenSSL::Digest[https://ruby.github.io/openssl/OpenSSL/Digest.html].
24
+ # Subclasses need only set an appropriate +DIGEST_NAME+ constant.
25
+ #
26
+ # === SCRAM algorithm
27
+ #
28
+ # See the documentation and method definitions on ScramAlgorithm for an
29
+ # overview of the algorithm. The different mechanisms differ only by
30
+ # which hash function that is used (or by support for channel binding with
31
+ # +-PLUS+).
32
+ #
33
+ # See also the methods on GS2Header.
34
+ #
35
+ # ==== Server messages
36
+ #
37
+ # As server messages are received, they are validated and loaded into
38
+ # the various attributes, e.g: #snonce, #salt, #iterations, #verifier,
39
+ # #server_error, etc.
40
+ #
41
+ # Unlike many other SASL mechanisms, the +SCRAM-*+ family supports mutual
42
+ # authentication and can return server error data in the server messages.
43
+ # If #process raises an Error for the server-final-message, then
44
+ # server_error may contain error details.
45
+ #
46
+ # === TLS Channel binding
47
+ #
48
+ # <em>The <tt>SCRAM-*-PLUS</tt> mechanisms and channel binding are not
49
+ # supported yet.</em>
50
+ #
51
+ # === Caching SCRAM secrets
52
+ #
53
+ # <em>Caching of salted_password, client_key, stored_key, and server_key
54
+ # is not supported yet.</em>
55
+ #
56
+ class ScramAuthenticator
57
+ include GS2Header
58
+ include ScramAlgorithm
59
+
60
+ # :call-seq:
61
+ # new(username, password, **options) -> auth_ctx
62
+ # new(username:, password:, **options) -> auth_ctx
63
+ #
64
+ # Creates an authenticator for one of the "+SCRAM-*+" SASL mechanisms.
65
+ # Each subclass defines #digest to match a specific mechanism.
66
+ #
67
+ # Called by Net::IMAP#authenticate and similar methods on other clients.
68
+ #
69
+ # === Parameters
70
+ #
71
+ # * #username ― Identity whose #password is used. Aliased as #authcid.
72
+ # * #password ― Password or passphrase associated with this #username.
73
+ # * #authzid ― Alternate identity to act as or on behalf of. Optional.
74
+ # * #min_iterations - Overrides the default value (4096). Optional.
75
+ #
76
+ # See the documentation on the corresponding attributes for more.
77
+ def initialize(username_arg = nil, password_arg = nil,
78
+ username: nil, password: nil, authcid: nil, authzid: nil,
79
+ min_iterations: 4096, # see both RFC5802 and RFC7677
80
+ cnonce: nil, # must only be set in tests
81
+ **options)
82
+ @username = username || username_arg || authcid or
83
+ raise ArgumentError, "missing username (authcid)"
84
+ [username, username_arg, authcid].compact.count == 1 or
85
+ raise ArgumentError, "conflicting values for username (authcid)"
86
+ @password = password || password_arg or
87
+ raise ArgumentError, "missing password"
88
+ [password, password_arg].compact.count == 1 or
89
+ raise ArgumentError, "conflicting values for password"
90
+ @authzid = authzid
91
+
92
+ @min_iterations = Integer min_iterations
93
+ @min_iterations.positive? or
94
+ raise ArgumentError, "min_iterations must be positive"
95
+ @cnonce = cnonce || SecureRandom.base64(32)
96
+ end
97
+
98
+ # Authentication identity: the identity that matches the #password.
99
+ attr_reader :username
100
+ alias authcid username
101
+
102
+ # A password or passphrase that matches the #username.
103
+ attr_reader :password
104
+
105
+ # Authorization identity: an identity to act as or on behalf of. The
106
+ # identity form is application protocol specific. If not provided or
107
+ # left blank, the server derives an authorization identity from the
108
+ # authentication identity. For example, an administrator or superuser
109
+ # might take on another role:
110
+ #
111
+ # imap.authenticate "SCRAM-SHA-256", "root", passwd, authzid: "user"
112
+ #
113
+ # The server is responsible for verifying the client's credentials and
114
+ # verifying that the identity it associates with the client's
115
+ # authentication identity is allowed to act as (or on behalf of) the
116
+ # authorization identity.
117
+ attr_reader :authzid
118
+
119
+ # The minimal allowed iteration count. Lower #iterations will raise an
120
+ # Error.
121
+ attr_reader :min_iterations
122
+
123
+ # The client nonce, generated by SecureRandom
124
+ attr_reader :cnonce
125
+
126
+ # The server nonce, which must start with #cnonce
127
+ attr_reader :snonce
128
+
129
+ # The salt used by the server for this user
130
+ attr_reader :salt
131
+
132
+ # The iteration count for the selected hash function and user
133
+ attr_reader :iterations
134
+
135
+ # An error reported by the server during the \SASL exchange.
136
+ #
137
+ # Does not include errors reported by the protocol, e.g.
138
+ # Net::IMAP::NoResponseError.
139
+ attr_reader :server_error
140
+
141
+ # Returns a new OpenSSL::Digest object, set to the appropriate hash
142
+ # function for the chosen mechanism.
143
+ #
144
+ # <em>The class's +DIGEST_NAME+ constant must be set to the name of an
145
+ # algorithm supported by OpenSSL::Digest.</em>
146
+ def digest; OpenSSL::Digest.new self.class::DIGEST_NAME end
147
+
148
+ # See {RFC5802 §7}[https://www.rfc-editor.org/rfc/rfc5802#section-7]
149
+ # +client-first-message+.
150
+ def initial_client_response
151
+ "#{gs2_header}#{client_first_message_bare}"
152
+ end
153
+
154
+ # responds to the server's challenges
155
+ def process(challenge)
156
+ case (@state ||= :initial_client_response)
157
+ when :initial_client_response
158
+ initial_client_response.tap { @state = :server_first_message }
159
+ when :server_first_message
160
+ recv_server_first_message challenge
161
+ final_message_with_proof.tap { @state = :server_final_message }
162
+ when :server_final_message
163
+ recv_server_final_message challenge
164
+ "".tap { @state = :done }
165
+ else
166
+ raise Error, "server sent after complete, %p" % [challenge]
167
+ end
168
+ rescue Exception => ex
169
+ @state = ex
170
+ raise
171
+ end
172
+
173
+ # Is the authentication exchange complete?
174
+ #
175
+ # If false, another server continuation is required.
176
+ def done?; @state == :done end
177
+
178
+ private
179
+
180
+ # Need to store this for auth_message
181
+ attr_reader :server_first_message
182
+
183
+ def format_message(hash) hash.map { _1.join("=") }.join(",") end
184
+
185
+ def recv_server_first_message(server_first_message)
186
+ @server_first_message = server_first_message
187
+ sparams = parse_challenge server_first_message
188
+ @snonce = sparams["r"] or
189
+ raise Error, "server did not send nonce"
190
+ @salt = sparams["s"]&.unpack1("m") or
191
+ raise Error, "server did not send salt"
192
+ @iterations = sparams["i"]&.then {|i| Integer i } or
193
+ raise Error, "server did not send iteration count"
194
+ min_iterations <= iterations or
195
+ raise Error, "too few iterations: %d" % [iterations]
196
+ mext = sparams["m"] and
197
+ raise Error, "mandatory extension: %p" % [mext]
198
+ snonce.start_with? cnonce or
199
+ raise Error, "invalid server nonce"
200
+ end
201
+
202
+ def recv_server_final_message(server_final_message)
203
+ sparams = parse_challenge server_final_message
204
+ @server_error = sparams["e"] and
205
+ raise Error, "server error: %s" % [server_error]
206
+ verifier = sparams["v"].unpack1("m") or
207
+ raise Error, "server did not send verifier"
208
+ verifier == server_signature or
209
+ raise Error, "server verify failed: %p != %p" % [
210
+ server_signature, verifier
211
+ ]
212
+ end
213
+
214
+ # See {RFC5802 §7}[https://www.rfc-editor.org/rfc/rfc5802#section-7]
215
+ # +client-first-message-bare+.
216
+ def client_first_message_bare
217
+ @client_first_message_bare ||=
218
+ format_message(n: gs2_saslname_encode(SASL.saslprep(username)),
219
+ r: cnonce)
220
+ end
221
+
222
+ # See {RFC5802 §7}[https://www.rfc-editor.org/rfc/rfc5802#section-7]
223
+ # +client-final-message+.
224
+ def final_message_with_proof
225
+ proof = [client_proof].pack("m0")
226
+ "#{client_final_message_without_proof},p=#{proof}"
227
+ end
228
+
229
+ # See {RFC5802 §7}[https://www.rfc-editor.org/rfc/rfc5802#section-7]
230
+ # +client-final-message-without-proof+.
231
+ def client_final_message_without_proof
232
+ @client_final_message_without_proof ||=
233
+ format_message(c: [cbind_input].pack("m0"), # channel-binding
234
+ r: snonce) # nonce
235
+ end
236
+
237
+ # See {RFC5802 §7}[https://www.rfc-editor.org/rfc/rfc5802#section-7]
238
+ # +cbind-input+.
239
+ #
240
+ # >>>
241
+ # *TODO:* implement channel binding, appending +cbind-data+ here.
242
+ alias cbind_input gs2_header
243
+
244
+ # RFC5802 specifies "that the order of attributes in client or server
245
+ # messages is fixed, with the exception of extension attributes", but
246
+ # this parses it simply as a hash, without respect to order. Note that
247
+ # repeated keys (violating the spec) will use the last value.
248
+ def parse_challenge(challenge)
249
+ challenge.split(/,/).to_h {|pair| pair.split(/=/, 2) }
250
+ rescue ArgumentError
251
+ raise Error, "unparsable challenge: %p" % [challenge]
252
+ end
253
+
254
+ end
255
+
256
+ # Authenticator for the "+SCRAM-SHA-1+" SASL mechanism, defined in
257
+ # RFC5802[https://tools.ietf.org/html/rfc5802].
258
+ #
259
+ # Uses the "SHA-1" digest algorithm from OpenSSL::Digest.
260
+ #
261
+ # See ScramAuthenticator.
262
+ class ScramSHA1Authenticator < ScramAuthenticator
263
+ DIGEST_NAME = "SHA1"
264
+ end
265
+
266
+ # Authenticator for the "+SCRAM-SHA-256+" SASL mechanism, defined in
267
+ # RFC7677[https://tools.ietf.org/html/rfc7677].
268
+ #
269
+ # Uses the "SHA-256" digest algorithm from OpenSSL::Digest.
270
+ #
271
+ # See ScramAuthenticator.
272
+ class ScramSHA256Authenticator < ScramAuthenticator
273
+ DIGEST_NAME = "SHA256"
274
+ end
275
+
276
+ end
277
+ end
278
+ end
@@ -1,72 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "stringprep_tables"
4
-
5
3
  module Net::IMAP::SASL
6
4
 
7
- # Regexps and utility methods for implementing stringprep profiles. The
8
- # \StringPrep algorithm is defined by
9
- # {RFC-3454}[https://www.rfc-editor.org/rfc/rfc3454.html]. Each
10
- # codepoint table defined in the RFC-3454 appendices is matched by a Regexp
11
- # defined in this module.
12
- #--
13
- # TODO: generic StringPrep mapping (not needed for SASLprep implementation)
14
- #++
15
- module StringPrep
16
-
17
- # Returns a Regexp matching the given +table+ name.
18
- def self.[](table)
19
- TABLE_REGEXPS.fetch(table)
20
- end
21
-
22
- module_function
23
-
24
- # Checks +string+ for any codepoint in +tables+. Raises a
25
- # ProhibitedCodepoint describing the first matching table.
26
- #
27
- # Also checks bidirectional characters, when <tt>bidi: true</tt>, which may
28
- # raise a BidiStringError.
29
- #
30
- # +profile+ is an optional string which will be added to any exception that
31
- # is raised (it does not affect behavior).
32
- def check_prohibited!(string, *tables, bidi: false, profile: nil)
33
- tables = TABLE_TITLES.keys.grep(/^C/) if tables.empty?
34
- tables |= %w[C.8] if bidi
35
- table = tables.find {|t| TABLE_REGEXPS[t].match?(string) }
36
- raise ProhibitedCodepoint.new(
37
- table, string: string, profile: nil
38
- ) if table
39
- check_bidi!(string, profile: profile) if bidi
40
- end
41
-
42
- # Checks that +string+ obeys all of the "Bidirectional Characters"
43
- # requirements in RFC-3454, §6:
44
- #
45
- # * The characters in \StringPrep\[\"C.8\"] MUST be prohibited
46
- # * If a string contains any RandALCat character, the string MUST NOT
47
- # contain any LCat character.
48
- # * If a string contains any RandALCat character, a RandALCat
49
- # character MUST be the first character of the string, and a
50
- # RandALCat character MUST be the last character of the string.
51
- #
52
- # This is usually combined with #check_prohibited!, so table "C.8" is only
53
- # checked when <tt>c_8: true</tt>.
54
- #
55
- # Raises either ProhibitedCodepoint or BidiStringError unless all
56
- # requirements are met. +profile+ is an optional string which will be
57
- # added to any exception that is raised (it does not affect behavior).
58
- def check_bidi!(string, c_8: false, profile: nil)
59
- check_prohibited!(string, "C.8", profile: profile) if c_8
60
- if BIDI_FAILS_REQ2.match?(string)
61
- raise BidiStringError.new(
62
- BIDI_DESC_REQ2, string: string, profile: profile,
63
- )
64
- elsif BIDI_FAILS_REQ3.match?(string)
65
- raise BidiStringError.new(
66
- BIDI_DESC_REQ3, string: string, profile: profile,
67
- )
68
- end
69
- end
5
+ # Alias for Net::IMAP::StringPrep::SASLprep.
6
+ SASLprep = Net::IMAP::StringPrep::SASLprep
7
+ StringPrep = Net::IMAP::StringPrep # :nodoc:
8
+ BidiStringError = Net::IMAP::StringPrep::BidiStringError # :nodoc:
9
+ ProhibitedCodepoint = Net::IMAP::StringPrep::ProhibitedCodepoint # :nodoc:
10
+ StringPrepError = Net::IMAP::StringPrep::StringPrepError # :nodoc:
70
11
 
71
- end
72
12
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Authenticator for the "+XOAUTH2+" SASL mechanism. This mechanism was
4
+ # originally created for GMail and widely adopted by hosted email providers.
5
+ # +XOAUTH2+ has been documented by
6
+ # Google[https://developers.google.com/gmail/imap/xoauth2-protocol] and
7
+ # Microsoft[https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth].
8
+ #
9
+ # This mechanism requires an OAuth2 +access_token+ which has been authorized
10
+ # with the appropriate OAuth2 scopes to access IMAP. These scopes are not
11
+ # standardized---consult each email service provider's documentation.
12
+ #
13
+ # Although this mechanism was never standardized and has been obsoleted by
14
+ # "+OAUTHBEARER+", it is still very widely supported.
15
+ #
16
+ # See Net::IMAP::SASL:: OAuthBearerAuthenticator.
17
+ class Net::IMAP::SASL::XOAuth2Authenticator
18
+
19
+ # It is unclear from {Google's original XOAUTH2
20
+ # documentation}[https://developers.google.com/gmail/imap/xoauth2-protocol],
21
+ # whether "User" refers to the authentication identity (+authcid+) or the
22
+ # authorization identity (+authzid+). It appears to behave as +authzid+.
23
+ #
24
+ # {Microsoft's documentation for shared
25
+ # mailboxes}[https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth#sasl-xoauth2-authentication-for-shared-mailboxes-in-office-365]
26
+ # clearly indicate that the Office 365 server interprets it as the
27
+ # authorization identity.
28
+ attr_reader :username
29
+
30
+ # An OAuth2 access token which has been authorized with the appropriate OAuth2
31
+ # scopes to use the service for #username.
32
+ attr_reader :oauth2_token
33
+
34
+ # :call-seq:
35
+ # new(username, oauth2_token, **) -> authenticator
36
+ # new(username:, oauth2_token:, **) -> authenticator
37
+ #
38
+ # Creates an Authenticator for the "+XOAUTH2+" SASL mechanism, as specified by
39
+ # Google[https://developers.google.com/gmail/imap/xoauth2-protocol],
40
+ # Microsoft[https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth]
41
+ # and Yahoo[https://senders.yahooinc.com/developer/documentation].
42
+ #
43
+ # === Properties
44
+ #
45
+ # * #username --- the username for the account being accessed.
46
+ # * #oauth2_token --- An OAuth2.0 access token which is authorized to access
47
+ # the service for #username.
48
+ #
49
+ # See the documentation for each attribute for more details.
50
+ def initialize(user = nil, token = nil, username: nil, oauth2_token: nil, **)
51
+ @username = username || user or
52
+ raise ArgumentError, "missing username"
53
+ @oauth2_token = oauth2_token || token or
54
+ raise ArgumentError, "missing oauth2_token"
55
+ [username, user].compact.count == 1 or
56
+ raise ArgumentError, "conflicting values for username"
57
+ [oauth2_token, token].compact.count == 1 or
58
+ raise ArgumentError, "conflicting values for oauth2_token"
59
+ @done = false
60
+ end
61
+
62
+ # :call-seq:
63
+ # initial_response? -> true
64
+ #
65
+ # +PLAIN+ can send an initial client response.
66
+ def initial_response?; true end
67
+
68
+ # Returns the XOAUTH2 formatted response, which combines the +username+
69
+ # with the +oauth2_token+.
70
+ def process(_data)
71
+ build_oauth2_string(@username, @oauth2_token)
72
+ ensure
73
+ @done = true
74
+ end
75
+
76
+ # Returns true when the initial client response was sent.
77
+ #
78
+ # The authentication should not succeed unless this returns true, but it
79
+ # does *not* indicate success.
80
+ def done?; @done end
81
+
82
+ private
83
+
84
+ def build_oauth2_string(username, oauth2_token)
85
+ format("user=%s\1auth=Bearer %s\1\1", username, oauth2_token)
86
+ end
87
+
88
+ end
data/lib/net/imap/sasl.rb CHANGED
@@ -6,12 +6,11 @@ module Net
6
6
  # Pluggable authentication mechanisms for protocols which support SASL
7
7
  # (Simple Authentication and Security Layer), such as IMAP4, SMTP, LDAP, and
8
8
  # XMPP. {RFC-4422}[https://tools.ietf.org/html/rfc4422] specifies the
9
- # common SASL framework and the +EXTERNAL+ mechanism, and the
10
- # {SASL mechanism registry}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
11
- # lists the specification for others.
12
- #
13
- # "SASL is conceptually a framework that provides an abstraction layer
14
- # between protocols and mechanisms as illustrated in the following diagram."
9
+ # common \SASL framework:
10
+ # >>>
11
+ # SASL is conceptually a framework that provides an abstraction layer
12
+ # between protocols and mechanisms as illustrated in the following
13
+ # diagram.
15
14
  #
16
15
  # SMTP LDAP XMPP Other protocols ...
17
16
  # \ | | /
@@ -21,58 +20,160 @@ module Net
21
20
  # / | | \
22
21
  # EXTERNAL GSSAPI PLAIN Other mechanisms ...
23
22
  #
23
+ # Net::IMAP uses SASL via the Net::IMAP#authenticate method.
24
+ #
25
+ # == Mechanisms
26
+ #
27
+ # Each mechanism has different properties and requirements. Please consult
28
+ # the documentation for the specific mechanisms you are using:
29
+ #
30
+ # +ANONYMOUS+::
31
+ # See AnonymousAuthenticator.
32
+ #
33
+ # Allows the user to gain access to public services or resources without
34
+ # authenticating or disclosing an identity.
35
+ #
36
+ # +EXTERNAL+::
37
+ # See ExternalAuthenticator.
38
+ #
39
+ # Authenticates using already established credentials, such as a TLS
40
+ # certificate or IPsec.
41
+ #
42
+ # +OAUTHBEARER+::
43
+ # See OAuthBearerAuthenticator.
44
+ #
45
+ # Login using an OAuth2 Bearer token. This is the standard mechanism
46
+ # for using OAuth2 with \SASL, but it is not yet deployed as widely as
47
+ # +XOAUTH2+.
48
+ #
49
+ # +PLAIN+::
50
+ # See PlainAuthenticator.
51
+ #
52
+ # Login using clear-text username and password.
53
+ #
54
+ # +SCRAM-SHA-1+::
55
+ # +SCRAM-SHA-256+::
56
+ # See ScramAuthenticator.
57
+ #
58
+ # Login by username and password. The password is not sent to the
59
+ # server but is used in a salted challenge/response exchange.
60
+ # +SCRAM-SHA-1+ and +SCRAM-SHA-256+ are directly supported by
61
+ # Net::IMAP::SASL. New authenticators can easily be added for any other
62
+ # <tt>SCRAM-*</tt> mechanism if the digest algorithm is supported by
63
+ # OpenSSL::Digest.
64
+ #
65
+ # +XOAUTH2+::
66
+ # See XOAuth2Authenticator.
67
+ #
68
+ # Login using a username and an OAuth2 access token. Non-standard and
69
+ # obsoleted by +OAUTHBEARER+, but widely supported.
70
+ #
71
+ # See the {SASL mechanism
72
+ # registry}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
73
+ # for a list of all SASL mechanisms and their specifications. To register
74
+ # new authenticators, see Authenticators.
75
+ #
76
+ # === Deprecated mechanisms
77
+ #
78
+ # <em>Obsolete mechanisms should be avoided, but are still available for
79
+ # backwards compatibility.</em>
80
+ #
81
+ # >>>
82
+ # For +DIGEST-MD5+ see DigestMD5Authenticator.
83
+ #
84
+ # For +LOGIN+, see LoginAuthenticator.
85
+ #
86
+ # For +CRAM-MD5+, see CramMD5Authenticator.
87
+ #
88
+ # <em>Using a deprecated mechanism will print a warning.</em>
89
+ #
24
90
  module SASL
91
+ # Exception class for any client error detected during the authentication
92
+ # exchange.
93
+ #
94
+ # When the _server_ reports an authentication failure, it will respond
95
+ # with a protocol specific error instead, e.g: +BAD+ or +NO+ in IMAP.
96
+ #
97
+ # When the client encounters any error, it *must* consider the
98
+ # authentication exchange to be unsuccessful and it might need to drop the
99
+ # connection. For example, if the server reports that the authentication
100
+ # exchange was successful or the protocol does not allow additional
101
+ # authentication attempts.
102
+ Error = Class.new(StandardError)
25
103
 
26
- # autoloading to avoid loading all of the regexps when they aren't used.
104
+ # Indicates an authentication exchange that will be or has been canceled
105
+ # by the client, not due to any error or failure during processing.
106
+ AuthenticationCanceled = Class.new(Error)
27
107
 
28
- autoload :StringPrep, File.expand_path("sasl/stringprep", __dir__)
29
- autoload :SASLprep, File.expand_path("#{__dir__}/sasl/saslprep", __dir__)
108
+ # Indicates an error when processing a server challenge, e.g: an invalid
109
+ # or unparsable challenge. An underlying exception may be available as
110
+ # the exception's #cause.
111
+ AuthenticationError = Class.new(Error)
30
112
 
31
- # ArgumentError raised when +string+ is invalid for the stringprep
32
- # +profile+.
33
- class StringPrepError < ArgumentError
34
- attr_reader :string, :profile
113
+ # Indicates that authentication cannot proceed because one of the server's
114
+ # messages has not passed integrity checks.
115
+ AuthenticationFailed = Class.new(Error)
35
116
 
36
- def initialize(*args, string: nil, profile: nil)
37
- @string = -string.to_str unless string.nil?
38
- @profile = -profile.to_str unless profile.nil?
39
- super(*args)
40
- end
41
- end
117
+ # Indicates that authentication cannot proceed because one of the server's
118
+ # ended authentication prematurely.
119
+ class AuthenticationIncomplete < AuthenticationFailed
120
+ # The success response from the server
121
+ attr_reader :response
42
122
 
43
- # StringPrepError raised when +string+ contains a codepoint prohibited by
44
- # +table+.
45
- class ProhibitedCodepoint < StringPrepError
46
- attr_reader :table
47
-
48
- def initialize(table, *args, **kwargs)
49
- @table = -table.to_str
50
- details = (title = StringPrep::TABLE_TITLES[table]) ?
51
- "%s [%s]" % [title, table] : table
52
- message = "String contains a prohibited codepoint: %s" % [details]
53
- super(message, *args, **kwargs)
123
+ def initialize(response, message = "authentication ended prematurely")
124
+ super(message)
125
+ @response = response
54
126
  end
55
127
  end
56
128
 
57
- # StringPrepError raised when +string+ contains bidirectional characters
58
- # which violate the StringPrep requirements.
59
- class BidiStringError < StringPrepError
129
+ # autoloading to avoid loading all of the regexps when they aren't used.
130
+ sasl_stringprep_rb = File.expand_path("sasl/stringprep", __dir__)
131
+ autoload :StringPrep, sasl_stringprep_rb
132
+ autoload :SASLprep, sasl_stringprep_rb
133
+ autoload :StringPrepError, sasl_stringprep_rb
134
+ autoload :ProhibitedCodepoint, sasl_stringprep_rb
135
+ autoload :BidiStringError, sasl_stringprep_rb
136
+
137
+ sasl_dir = File.expand_path("sasl", __dir__)
138
+ autoload :AuthenticationExchange, "#{sasl_dir}/authentication_exchange"
139
+ autoload :ClientAdapter, "#{sasl_dir}/client_adapter"
140
+ autoload :ProtocolAdapters, "#{sasl_dir}/protocol_adapters"
141
+
142
+ autoload :Authenticators, "#{sasl_dir}/authenticators"
143
+ autoload :GS2Header, "#{sasl_dir}/gs2_header"
144
+ autoload :ScramAlgorithm, "#{sasl_dir}/scram_algorithm"
145
+
146
+ autoload :AnonymousAuthenticator, "#{sasl_dir}/anonymous_authenticator"
147
+ autoload :ExternalAuthenticator, "#{sasl_dir}/external_authenticator"
148
+ autoload :OAuthBearerAuthenticator, "#{sasl_dir}/oauthbearer_authenticator"
149
+ autoload :PlainAuthenticator, "#{sasl_dir}/plain_authenticator"
150
+ autoload :ScramAuthenticator, "#{sasl_dir}/scram_authenticator"
151
+ autoload :ScramSHA1Authenticator, "#{sasl_dir}/scram_authenticator"
152
+ autoload :ScramSHA256Authenticator, "#{sasl_dir}/scram_authenticator"
153
+ autoload :XOAuth2Authenticator, "#{sasl_dir}/xoauth2_authenticator"
154
+
155
+ autoload :CramMD5Authenticator, "#{sasl_dir}/cram_md5_authenticator"
156
+ autoload :DigestMD5Authenticator, "#{sasl_dir}/digest_md5_authenticator"
157
+ autoload :LoginAuthenticator, "#{sasl_dir}/login_authenticator"
158
+
159
+ # Returns the default global SASL::Authenticators instance.
160
+ def self.authenticators; @authenticators ||= Authenticators.new end
161
+
162
+ # Delegates to <tt>registry.new</tt> See Authenticators#new.
163
+ def self.authenticator(*args, registry: authenticators, **kwargs, &block)
164
+ registry.new(*args, **kwargs, &block)
60
165
  end
61
166
 
62
- #--
63
- # We could just extend SASLprep module directly. It's done this way so
64
- # SASLprep can be lazily autoloaded. Most users won't need it.
65
- #++
66
- extend self
167
+ # Delegates to ::authenticators. See Authenticators#add_authenticator.
168
+ def self.add_authenticator(...) authenticators.add_authenticator(...) end
169
+
170
+ module_function
67
171
 
68
- # See SASLprep#saslprep.
172
+ # See Net::IMAP::StringPrep::SASLprep#saslprep.
69
173
  def saslprep(string, **opts)
70
- SASLprep.saslprep(string, **opts)
174
+ Net::IMAP::StringPrep::SASLprep.saslprep(string, **opts)
71
175
  end
72
176
 
73
177
  end
74
178
  end
75
-
76
179
  end
77
-
78
- Net::IMAP.extend Net::IMAP::SASL
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP
5
+
6
+ # Experimental
7
+ class SASLAdapter < SASL::ClientAdapter
8
+ include SASL::ProtocolAdapters::IMAP
9
+
10
+ RESPONSE_ERRORS = [NoResponseError, BadResponseError, ByeResponseError]
11
+ .freeze
12
+
13
+ def response_errors; RESPONSE_ERRORS end
14
+ def sasl_ir_capable?; client.capable?("SASL-IR") end
15
+ def auth_capable?(mechanism); client.auth_capable?(mechanism) end
16
+ def drop_connection; client.logout! end
17
+ def drop_connection!; client.disconnect end
18
+ end
19
+
20
+ end
21
+ end