net-imap 0.4.19 → 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.

Potentially problematic release.


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

@@ -4,44 +4,76 @@ module Net
4
4
  class IMAP
5
5
  module SASL
6
6
 
7
- # This API is *experimental*, and may change.
7
+ # AuthenticationExchange is used internally by Net::IMAP#authenticate.
8
+ # But the API is still *experimental*, and may change.
8
9
  #
9
10
  # TODO: catch exceptions in #process and send #cancel_response.
10
11
  # TODO: raise an error if the command succeeds after being canceled.
11
12
  # TODO: use with more clients, to verify the API can accommodate them.
13
+ # TODO: pass ClientAdapter#service to SASL.authenticator
12
14
  #
13
- # Create an AuthenticationExchange from a client adapter and a mechanism
14
- # authenticator:
15
- # def authenticate(mechanism, ...)
16
- # authenticator = SASL.authenticator(mechanism, ...)
17
- # SASL::AuthenticationExchange.new(
18
- # sasl_adapter, mechanism, authenticator
19
- # ).authenticate
20
- # end
21
- #
22
- # private
15
+ # An AuthenticationExchange represents a single attempt to authenticate
16
+ # a SASL client to a SASL server. It is created from a client adapter, a
17
+ # mechanism name, and a mechanism authenticator. When #authenticate is
18
+ # called, it will send the appropriate authenticate command to the server,
19
+ # returning the client response on success and raising an exception on
20
+ # failure.
23
21
  #
24
- # def sasl_adapter = MyClientAdapter.new(self, &method(:send_command))
22
+ # In most cases, the client will not need to use
23
+ # SASL::AuthenticationExchange directly at all. Instead, use
24
+ # SASL::ClientAdapter#authenticate. If customizations are needed, the
25
+ # custom client adapter is probably the best place for that code.
25
26
  #
26
- # Or delegate creation of the authenticator to ::build:
27
27
  # def authenticate(...)
28
- # SASL::AuthenticationExchange.build(sasl_adapter, ...)
29
- # .authenticate
28
+ # MyClient::SASLAdapter.new(self).authenticate(...)
30
29
  # end
31
30
  #
32
- # As a convenience, ::authenticate combines ::build and #authenticate:
31
+ # SASL::ClientAdapter#authenticate delegates to ::authenticate, like so:
32
+ #
33
33
  # def authenticate(...)
34
+ # sasl_adapter = MyClient::SASLAdapter.new(self)
34
35
  # SASL::AuthenticationExchange.authenticate(sasl_adapter, ...)
35
36
  # end
36
37
  #
37
- # Likewise, ClientAdapter#authenticate delegates to #authenticate:
38
- # def authenticate(...) = sasl_adapter.authenticate(...)
38
+ # ::authenticate simply delegates to ::build and #authenticate, like so:
39
+ #
40
+ # def authenticate(...)
41
+ # sasl_adapter = MyClient::SASLAdapter.new(self)
42
+ # SASL::AuthenticationExchange
43
+ # .build(sasl_adapter, ...)
44
+ # .authenticate
45
+ # end
46
+ #
47
+ # And ::build delegates to SASL.authenticator and ::new, like so:
48
+ #
49
+ # def authenticate(mechanism, ...)
50
+ # sasl_adapter = MyClient::SASLAdapter.new(self)
51
+ # authenticator = SASL.authenticator(mechanism, ...)
52
+ # SASL::AuthenticationExchange
53
+ # .new(sasl_adapter, mechanism, authenticator)
54
+ # .authenticate
55
+ # end
39
56
  #
40
57
  class AuthenticationExchange
41
58
  # Convenience method for <tt>build(...).authenticate</tt>
59
+ #
60
+ # See also: SASL::ClientAdapter#authenticate
42
61
  def self.authenticate(...) build(...).authenticate end
43
62
 
44
- # Use +registry+ to override the global Authenticators registry.
63
+ # Convenience method to combine the creation of a new authenticator and
64
+ # a new Authentication exchange.
65
+ #
66
+ # +client+ must be an instance of SASL::ClientAdapter.
67
+ #
68
+ # +mechanism+ must be a SASL mechanism name, as a string or symbol.
69
+ #
70
+ # +sasl_ir+ allows or disallows sending an "initial response", depending
71
+ # also on whether the server capabilities, mechanism authenticator, and
72
+ # client adapter all support it. Defaults to +true+.
73
+ #
74
+ # +mechanism+, +args+, +kwargs+, and +block+ are all forwarded to
75
+ # SASL.authenticator. Use the +registry+ kwarg to override the global
76
+ # SASL::Authenticators registry.
45
77
  def self.build(client, mechanism, *args, sasl_ir: true, **kwargs, &block)
46
78
  authenticator = SASL.authenticator(mechanism, *args, **kwargs, &block)
47
79
  new(client, mechanism, authenticator, sasl_ir: sasl_ir)
@@ -51,7 +83,7 @@ module Net
51
83
 
52
84
  def initialize(client, mechanism, authenticator, sasl_ir: true)
53
85
  @client = client
54
- @mechanism = -mechanism.to_s.upcase.tr(?_, ?-)
86
+ @mechanism = Authenticators.normalize_name(mechanism)
55
87
  @authenticator = authenticator
56
88
  @sasl_ir = sasl_ir
57
89
  @processed = false
@@ -21,6 +21,10 @@ module Net::IMAP::SASL
21
21
  # ScramSHA1Authenticator for examples.
22
22
  class Authenticators
23
23
 
24
+ # Normalize the mechanism name as an uppercase string, with underscores
25
+ # converted to dashes.
26
+ def self.normalize_name(mechanism) -(mechanism.to_s.upcase.tr(?_, ?-)) end
27
+
24
28
  # Create a new Authenticators registry.
25
29
  #
26
30
  # This class is usually not instantiated directly. Use SASL.authenticators
@@ -65,7 +69,6 @@ module Net::IMAP::SASL
65
69
  # lazily loaded from <tt>Net::IMAP::SASL::#{name}Authenticator</tt> (case is
66
70
  # preserved and non-alphanumeric characters are removed..
67
71
  def add_authenticator(name, authenticator = nil)
68
- key = -name.to_s.upcase.tr(?_, ?-)
69
72
  authenticator ||= begin
70
73
  class_name = "#{name.gsub(/[^a-zA-Z0-9]/, "")}Authenticator".to_sym
71
74
  auth_class = nil
@@ -74,17 +77,18 @@ module Net::IMAP::SASL
74
77
  auth_class.new(*creds, **props, &block)
75
78
  }
76
79
  end
80
+ key = Authenticators.normalize_name(name)
77
81
  @authenticators[key] = authenticator
78
82
  end
79
83
 
80
84
  # Removes the authenticator registered for +name+
81
85
  def remove_authenticator(name)
82
- key = -name.to_s.upcase.tr(?_, ?-)
86
+ key = Authenticators.normalize_name(name)
83
87
  @authenticators.delete(key)
84
88
  end
85
89
 
86
90
  def mechanism?(name)
87
- key = -name.to_s.upcase.tr(?_, ?-)
91
+ key = Authenticators.normalize_name(name)
88
92
  @authenticators.key?(key)
89
93
  end
90
94
 
@@ -105,7 +109,7 @@ module Net::IMAP::SASL
105
109
  # only. Protocol client users should see refer to their client's
106
110
  # documentation, e.g. Net::IMAP#authenticate.
107
111
  def authenticator(mechanism, ...)
108
- key = -mechanism.to_s.upcase.tr(?_, ?-)
112
+ key = Authenticators.normalize_name(mechanism)
109
113
  auth = @authenticators.fetch(key) do
110
114
  raise ArgumentError, 'unknown auth type - "%s"' % key
111
115
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "forwardable"
4
+
3
5
  module Net
4
6
  class IMAP
5
7
  module SASL
@@ -8,42 +10,76 @@ module Net
8
10
  #
9
11
  # TODO: use with more clients, to verify the API can accommodate them.
10
12
  #
11
- # An abstract base class for implementing a SASL authentication exchange.
12
- # Different clients will each have their own adapter subclass, overridden
13
- # to match their needs.
13
+ # Represents the client to a SASL::AuthenticationExchange. By default,
14
+ # most methods simply delegate to #client. Clients should subclass
15
+ # SASL::ClientAdapter and override methods as needed to match the
16
+ # semantics of this API to their API.
14
17
  #
15
- # Although the default implementations _may_ be sufficient, subclasses
16
- # will probably need to override some methods. Additionally, subclasses
17
- # may need to include a protocol adapter mixin, if the default
18
+ # Subclasses should also include a protocol adapter mixin when the default
18
19
  # ProtocolAdapters::Generic isn't sufficient.
20
+ #
21
+ # === Protocol Requirements
22
+ #
23
+ # {RFC4422 §4}[https://www.rfc-editor.org/rfc/rfc4422.html#section-4]
24
+ # lists requirements for protocol specifications to offer SASL. Where
25
+ # possible, ClientAdapter delegates the handling of these requirements to
26
+ # SASL::ProtocolAdapters.
19
27
  class ClientAdapter
28
+ extend Forwardable
29
+
20
30
  include ProtocolAdapters::Generic
21
31
 
22
- attr_reader :client, :command_proc
32
+ # The client that handles communication with the protocol server.
33
+ #
34
+ # Most ClientAdapter methods are simply delegated to #client by default.
35
+ attr_reader :client
23
36
 
24
37
  # +command_proc+ can used to avoid exposing private methods on #client.
25
- # It should run a command with the arguments sent to it, yield each
26
- # continuation payload, respond to the server with the result of each
27
- # yield, and return the result. Non-successful results *MUST* raise an
28
- # exception. Exceptions in the block *MUST* cause the command to fail.
38
+ # It's value is set by the block that is passed to ::new, and it is used
39
+ # by the default implementation of #run_command. Subclasses that
40
+ # override #run_command may use #command_proc for any other purpose they
41
+ # find useful.
29
42
  #
30
- # Subclasses that override #run_command may use #command_proc for
31
- # other purposes.
43
+ # In the default implementation of #run_command, command_proc is called
44
+ # with the protocols authenticate +command+ name, the +mechanism+ name,
45
+ # an _optional_ +initial_response+ argument, and a +continuations+
46
+ # block. command_proc must run the protocol command with the arguments
47
+ # sent to it, _yield_ the payload of each continuation, respond to the
48
+ # continuation with the result of each _yield_, and _return_ the
49
+ # command's successful result. Non-successful results *MUST* raise
50
+ # an exception.
51
+ attr_reader :command_proc
52
+
53
+ # By default, this simply sets the #client and #command_proc attributes.
54
+ # Subclasses may override it, for example: to set the appropriate
55
+ # command_proc automatically.
32
56
  def initialize(client, &command_proc)
33
57
  @client, @command_proc = client, command_proc
34
58
  end
35
59
 
36
- # Delegates to AuthenticationExchange.authenticate.
60
+ # Attempt to authenticate #client to the server.
61
+ #
62
+ # By default, this simply delegates to
63
+ # AuthenticationExchange.authenticate.
37
64
  def authenticate(...) AuthenticationExchange.authenticate(self, ...) end
38
65
 
39
- # Do the protocol and server both support an initial response?
40
- def sasl_ir_capable?; client.sasl_ir_capable? end
66
+ ##
67
+ # method: sasl_ir_capable?
68
+ # Do the protocol, server, and client all support an initial response?
69
+ def_delegator :client, :sasl_ir_capable?
41
70
 
42
- # Does the server advertise support for the mechanism?
43
- def auth_capable?(mechanism); client.auth_capable?(mechanism) end
71
+ ##
72
+ # method: auth_capable?
73
+ # call-seq: auth_capable?(mechanism)
74
+ #
75
+ # Does the server advertise support for the +mechanism+?
76
+ def_delegator :client, :auth_capable?
44
77
 
45
- # Runs the authenticate command with +mechanism+ and +initial_response+.
46
- # When +initial_response+ is nil, an initial response must NOT be sent.
78
+ # Calls command_proc with +command_name+ (see
79
+ # SASL::ProtocolAdapters::Generic#command_name),
80
+ # +mechanism+, +initial_response+, and a +continuations_handler+ block.
81
+ # The +initial_response+ is optional; when it's nil, it won't be sent to
82
+ # command_proc.
47
83
  #
48
84
  # Yields each continuation payload, responds to the server with the
49
85
  # result of each yield, and returns the result. Non-successful results
@@ -51,21 +87,36 @@ module Net
51
87
  # command to fail.
52
88
  #
53
89
  # Subclasses that override this may use #command_proc differently.
54
- def run_command(mechanism, initial_response = nil, &block)
90
+ def run_command(mechanism, initial_response = nil, &continuations_handler)
55
91
  command_proc or raise Error, "initialize with block or override"
56
92
  args = [command_name, mechanism, initial_response].compact
57
- command_proc.call(*args, &block)
93
+ command_proc.call(*args, &continuations_handler)
58
94
  end
59
95
 
96
+ ##
97
+ # method: host
98
+ # The hostname to which the client connected.
99
+ def_delegator :client, :host
100
+
101
+ ##
102
+ # method: port
103
+ # The destination port to which the client connected.
104
+ def_delegator :client, :port
105
+
60
106
  # Returns an array of server responses errors raised by run_command.
61
107
  # Exceptions in this array won't drop the connection.
62
108
  def response_errors; [] end
63
109
 
64
- # Drop the connection gracefully.
65
- def drop_connection; client.drop_connection end
110
+ ##
111
+ # method: drop_connection
112
+ # Drop the connection gracefully, sending a "LOGOUT" command as needed.
113
+ def_delegator :client, :drop_connection
114
+
115
+ ##
116
+ # method: drop_connection!
117
+ # Drop the connection abruptly, closing the socket without logging out.
118
+ def_delegator :client, :drop_connection!
66
119
 
67
- # Drop the connection abruptly.
68
- def drop_connection!; client.drop_connection! end
69
120
  end
70
121
  end
71
122
  end
@@ -20,7 +20,7 @@ class Net::IMAP::SASL::CramMD5Authenticator
20
20
  warn_deprecation: true,
21
21
  **)
22
22
  if warn_deprecation
23
- warn "WARNING: CRAM-MD5 mechanism is deprecated." # TODO: recommend SCRAM
23
+ warn "WARNING: CRAM-MD5 mechanism is deprecated.", category: :deprecated
24
24
  end
25
25
  require "digest/md5"
26
26
  @user = authcid || username || user
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Net::IMAP authenticator for the "`DIGEST-MD5`" SASL mechanism type, specified
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 "WARNING: DIGEST-MD5 SASL mechanism was deprecated by RFC6331."
80
- # TODO: recommend SCRAM instead.
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
- c = StringScanner.new(challenge)
97
- while c.scan(/(?:\s*,)?\s*(\w+)=("(?:[^\\"]|\\.)*"|[^,]+)\s*/)
98
- k, v = c[1], c[2]
99
- if v =~ /^"(.*)"$/
100
- v = $1
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
- raise Net::IMAP::DataFormatError, "Bad Challenge: '#{challenge}'" unless c.eos? and sparams['qop']
109
- raise Net::IMAP::Error, "Server does not support auth (qop = #{sparams['qop'].join(',')})" unless sparams['qop'].include?("auth")
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 => sparams['nonce'],
113
- :username => @username,
114
- :realm => sparams['realm'],
115
- :cnonce => Digest::MD5.hexdigest("%.15f:%.15f:%d" % [Time.now.to_f, rand, Process.pid.to_s]),
116
- :'digest-uri' => 'imap/' + sparams['realm'],
117
- :qop => 'auth',
118
- :maxbuf => 65535,
119
- :nc => "%08d" % nc(sparams['nonce']),
120
- :charset => sparams['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
- # now, the real thing
126
- a0 = Digest::MD5.digest( [ response.values_at(:username, :realm), @password ].join(':') )
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
- # if at the second stage, return an empty string
146
- if challenge =~ /rspauth=/
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
- return @nc[nonce]
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