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,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP < Protocol
5
+ module SASL
6
+
7
+ # Authenticator for the "+ANONYMOUS+" SASL mechanism, as specified by
8
+ # RFC-4505[https://tools.ietf.org/html/rfc4505]. See
9
+ # Net::IMAP#authenticate.
10
+ class AnonymousAuthenticator
11
+
12
+ # An optional token sent for the +ANONYMOUS+ mechanism., up to 255 UTF-8
13
+ # characters in length.
14
+ #
15
+ # If it contains an "@" sign, the message must be a valid email address
16
+ # (+addr-spec+ from RFC-2822[https://tools.ietf.org/html/rfc2822]).
17
+ # Email syntax is _not_ validated by AnonymousAuthenticator.
18
+ #
19
+ # Otherwise, it can be any UTF8 string which is permitted by the
20
+ # StringPrep::Trace profile.
21
+ attr_reader :anonymous_message
22
+
23
+ # :call-seq:
24
+ # new(anonymous_message = "", **) -> authenticator
25
+ # new(anonymous_message: "", **) -> authenticator
26
+ #
27
+ # Creates an Authenticator for the "+ANONYMOUS+" SASL mechanism, as
28
+ # specified in RFC-4505[https://tools.ietf.org/html/rfc4505]. To use
29
+ # this, see Net::IMAP#authenticate or your client's authentication
30
+ # method.
31
+ #
32
+ # #anonymous_message is an optional message which is sent to the server.
33
+ # It may be sent as a positional argument or as a keyword argument.
34
+ #
35
+ # Any other keyword arguments are silently ignored.
36
+ def initialize(anon_msg = nil, anonymous_message: nil, **)
37
+ message = (anonymous_message || anon_msg || "").to_str
38
+ @anonymous_message = StringPrep::Trace.stringprep_trace message
39
+ if (size = @anonymous_message&.length)&.> 255
40
+ raise ArgumentError,
41
+ "anonymous_message is too long. (%d codepoints)" % [size]
42
+ end
43
+ @done = false
44
+ end
45
+
46
+ # :call-seq:
47
+ # initial_response? -> true
48
+ #
49
+ # +ANONYMOUS+ can send an initial client response.
50
+ def initial_response?; true end
51
+
52
+ # Returns #anonymous_message.
53
+ def process(_server_challenge_string)
54
+ anonymous_message
55
+ ensure
56
+ @done = true
57
+ end
58
+
59
+ # Returns true when the initial client response was sent.
60
+ #
61
+ # The authentication should not succeed unless this returns true, but it
62
+ # does *not* indicate success.
63
+ def done?; @done end
64
+
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP
5
+ module SASL
6
+
7
+ # This API is *experimental*, and may change.
8
+ #
9
+ # TODO: catch exceptions in #process and send #cancel_response.
10
+ # TODO: raise an error if the command succeeds after being canceled.
11
+ # TODO: use with more clients, to verify the API can accommodate them.
12
+ #
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
23
+ #
24
+ # def sasl_adapter = MyClientAdapter.new(self, &method(:send_command))
25
+ #
26
+ # Or delegate creation of the authenticator to ::build:
27
+ # def authenticate(...)
28
+ # SASL::AuthenticationExchange.build(sasl_adapter, ...)
29
+ # .authenticate
30
+ # end
31
+ #
32
+ # As a convenience, ::authenticate combines ::build and #authenticate:
33
+ # def authenticate(...)
34
+ # SASL::AuthenticationExchange.authenticate(sasl_adapter, ...)
35
+ # end
36
+ #
37
+ # Likewise, ClientAdapter#authenticate delegates to #authenticate:
38
+ # def authenticate(...) = sasl_adapter.authenticate(...)
39
+ #
40
+ class AuthenticationExchange
41
+ # Convenience method for <tt>build(...).authenticate</tt>
42
+ def self.authenticate(...) build(...).authenticate end
43
+
44
+ # Use +registry+ to override the global Authenticators registry.
45
+ def self.build(client, mechanism, *args, sasl_ir: true, **kwargs, &block)
46
+ authenticator = SASL.authenticator(mechanism, *args, **kwargs, &block)
47
+ new(client, mechanism, authenticator, sasl_ir: sasl_ir)
48
+ end
49
+
50
+ attr_reader :mechanism, :authenticator
51
+
52
+ def initialize(client, mechanism, authenticator, sasl_ir: true)
53
+ @client = client
54
+ @mechanism = -mechanism.to_s.upcase.tr(?_, ?-)
55
+ @authenticator = authenticator
56
+ @sasl_ir = sasl_ir
57
+ @processed = false
58
+ end
59
+
60
+ # Call #authenticate to execute an authentication exchange for #client
61
+ # using #authenticator. Authentication failures will raise an
62
+ # exception. Any exceptions other than those in RESPONSE_ERRORS will
63
+ # drop the connection.
64
+ def authenticate
65
+ client.run_command(mechanism, initial_response) { process _1 }
66
+ .tap { raise AuthenticationIncomplete, _1 unless done? }
67
+ rescue *client.response_errors
68
+ raise # but don't drop the connection
69
+ rescue
70
+ client.drop_connection
71
+ raise
72
+ rescue Exception # rubocop:disable Lint/RescueException
73
+ client.drop_connection!
74
+ raise
75
+ end
76
+
77
+ def send_initial_response?
78
+ @sasl_ir &&
79
+ authenticator.respond_to?(:initial_response?) &&
80
+ authenticator.initial_response? &&
81
+ client.sasl_ir_capable? &&
82
+ client.auth_capable?(mechanism)
83
+ end
84
+
85
+ def done?
86
+ authenticator.respond_to?(:done?) ? authenticator.done? : @processed
87
+ end
88
+
89
+ private
90
+
91
+ attr_reader :client
92
+
93
+ def initial_response
94
+ return unless send_initial_response?
95
+ client.encode_ir authenticator.process nil
96
+ end
97
+
98
+ def process(challenge)
99
+ client.encode authenticator.process client.decode challenge
100
+ ensure
101
+ @processed = true
102
+ end
103
+
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net::IMAP::SASL
4
+
5
+ # Registry for SASL authenticators
6
+ #
7
+ # Registered authenticators must respond to +#new+ or +#call+ (e.g. a class or
8
+ # a proc), receiving any credentials and options and returning an
9
+ # authenticator instance. The returned object represents a single
10
+ # authentication exchange and <em>must not</em> be reused for multiple
11
+ # authentication attempts.
12
+ #
13
+ # An authenticator instance object must respond to +#process+, receiving the
14
+ # server's challenge and returning the client's response. Optionally, it may
15
+ # also respond to +#initial_response?+ and +#done?+. When
16
+ # +#initial_response?+ returns +true+, +#process+ may be called the first
17
+ # time with +nil+. When +#done?+ returns +false+, the exchange is incomplete
18
+ # and an exception should be raised if the exchange terminates prematurely.
19
+ #
20
+ # See the source for PlainAuthenticator, XOAuth2Authenticator, and
21
+ # ScramSHA1Authenticator for examples.
22
+ class Authenticators
23
+
24
+ # Create a new Authenticators registry.
25
+ #
26
+ # This class is usually not instantiated directly. Use SASL.authenticators
27
+ # to reuse the default global registry.
28
+ #
29
+ # When +use_defaults+ is +false+, the registry will start empty. When
30
+ # +use_deprecated+ is +false+, deprecated authenticators will not be
31
+ # included with the defaults.
32
+ def initialize(use_defaults: true, use_deprecated: true)
33
+ @authenticators = {}
34
+ return unless use_defaults
35
+ add_authenticator "Anonymous"
36
+ add_authenticator "External"
37
+ add_authenticator "OAuthBearer"
38
+ add_authenticator "Plain"
39
+ add_authenticator "Scram-SHA-1"
40
+ add_authenticator "Scram-SHA-256"
41
+ add_authenticator "XOAuth2"
42
+ return unless use_deprecated
43
+ add_authenticator "Login" # deprecated
44
+ add_authenticator "Cram-MD5" # deprecated
45
+ add_authenticator "Digest-MD5" # deprecated
46
+ end
47
+
48
+ # Returns the names of all registered SASL mechanisms.
49
+ def names; @authenticators.keys end
50
+
51
+ # :call-seq:
52
+ # add_authenticator(mechanism)
53
+ # add_authenticator(mechanism, authenticator_class)
54
+ # add_authenticator(mechanism, authenticator_proc)
55
+ #
56
+ # Registers an authenticator for #authenticator to use. +mechanism+ is the
57
+ # name of the
58
+ # {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
59
+ # implemented by +authenticator_class+ (for instance, <tt>"PLAIN"</tt>).
60
+ #
61
+ # If +mechanism+ refers to an existing authenticator, a warning will be
62
+ # printed and the old authenticator will be replaced.
63
+ #
64
+ # When only a single argument is given, the authenticator class will be
65
+ # lazily loaded from <tt>Net::IMAP::SASL::#{name}Authenticator</tt> (case is
66
+ # preserved and non-alphanumeric characters are removed..
67
+ def add_authenticator(name, authenticator = nil)
68
+ key = -name.to_s.upcase.tr(?_, ?-)
69
+ authenticator ||= begin
70
+ class_name = "#{name.gsub(/[^a-zA-Z0-9]/, "")}Authenticator".to_sym
71
+ auth_class = nil
72
+ ->(*creds, **props, &block) {
73
+ auth_class ||= Net::IMAP::SASL.const_get(class_name)
74
+ auth_class.new(*creds, **props, &block)
75
+ }
76
+ end
77
+ @authenticators[key] = authenticator
78
+ end
79
+
80
+ # Removes the authenticator registered for +name+
81
+ def remove_authenticator(name)
82
+ key = -name.to_s.upcase.tr(?_, ?-)
83
+ @authenticators.delete(key)
84
+ end
85
+
86
+ def mechanism?(name)
87
+ key = -name.to_s.upcase.tr(?_, ?-)
88
+ @authenticators.key?(key)
89
+ end
90
+
91
+ # :call-seq:
92
+ # authenticator(mechanism, ...) -> auth_session
93
+ #
94
+ # Builds an authenticator instance using the authenticator registered to
95
+ # +mechanism+. The returned object represents a single authentication
96
+ # exchange and <em>must not</em> be reused for multiple authentication
97
+ # attempts.
98
+ #
99
+ # All arguments (except +mechanism+) are forwarded to the registered
100
+ # authenticator's +#new+ or +#call+ method. Each authenticator must
101
+ # document its own arguments.
102
+ #
103
+ # [Note]
104
+ # This method is intended for internal use by connection protocol code
105
+ # only. Protocol client users should see refer to their client's
106
+ # documentation, e.g. Net::IMAP#authenticate.
107
+ def authenticator(mechanism, ...)
108
+ key = -mechanism.to_s.upcase.tr(?_, ?-)
109
+ auth = @authenticators.fetch(key) do
110
+ raise ArgumentError, 'unknown auth type - "%s"' % key
111
+ end
112
+ auth.respond_to?(:new) ? auth.new(...) : auth.call(...)
113
+ end
114
+ alias new authenticator
115
+
116
+ end
117
+
118
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP
5
+ module SASL
6
+
7
+ # This API is *experimental*, and may change.
8
+ #
9
+ # TODO: use with more clients, to verify the API can accommodate them.
10
+ #
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.
14
+ #
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
+ # ProtocolAdapters::Generic isn't sufficient.
19
+ class ClientAdapter
20
+ include ProtocolAdapters::Generic
21
+
22
+ attr_reader :client, :command_proc
23
+
24
+ # +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.
29
+ #
30
+ # Subclasses that override #run_command may use #command_proc for
31
+ # other purposes.
32
+ def initialize(client, &command_proc)
33
+ @client, @command_proc = client, command_proc
34
+ end
35
+
36
+ # Delegates to AuthenticationExchange.authenticate.
37
+ def authenticate(...) AuthenticationExchange.authenticate(self, ...) end
38
+
39
+ # Do the protocol and server both support an initial response?
40
+ def sasl_ir_capable?; client.sasl_ir_capable? end
41
+
42
+ # Does the server advertise support for the mechanism?
43
+ def auth_capable?(mechanism); client.auth_capable?(mechanism) end
44
+
45
+ # Runs the authenticate command with +mechanism+ and +initial_response+.
46
+ # When +initial_response+ is nil, an initial response must NOT be sent.
47
+ #
48
+ # Yields each continuation payload, responds to the server with the
49
+ # result of each yield, and returns the result. Non-successful results
50
+ # *MUST* raise an exception. Exceptions in the block *MUST* cause the
51
+ # command to fail.
52
+ #
53
+ # Subclasses that override this may use #command_proc differently.
54
+ def run_command(mechanism, initial_response = nil, &block)
55
+ command_proc or raise Error, "initialize with block or override"
56
+ args = [command_name, mechanism, initial_response].compact
57
+ command_proc.call(*args, &block)
58
+ end
59
+
60
+ # Returns an array of server responses errors raised by run_command.
61
+ # Exceptions in this array won't drop the connection.
62
+ def response_errors; [] end
63
+
64
+ # Drop the connection gracefully.
65
+ def drop_connection; client.drop_connection end
66
+
67
+ # Drop the connection abruptly.
68
+ def drop_connection!; client.drop_connection! end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -13,14 +13,7 @@
13
13
  # Additionally, RFC8314[https://tools.ietf.org/html/rfc8314] discourage the use
14
14
  # of cleartext and recommends TLS version 1.2 or greater be used for all
15
15
  # traffic. With TLS +CRAM-MD5+ is okay, but so is +PLAIN+
16
- class Net::IMAP::CramMD5Authenticator
17
- def process(challenge)
18
- digest = hmac_md5(challenge, @password)
19
- return @user + " " + digest
20
- end
21
-
22
- private
23
-
16
+ class Net::IMAP::SASL::CramMD5Authenticator
24
17
  def initialize(user, password, warn_deprecation: true, **_ignored)
25
18
  if warn_deprecation
26
19
  warn "WARNING: CRAM-MD5 mechanism is deprecated." # TODO: recommend SCRAM
@@ -28,8 +21,22 @@ class Net::IMAP::CramMD5Authenticator
28
21
  require "digest/md5"
29
22
  @user = user
30
23
  @password = password
24
+ @done = false
25
+ end
26
+
27
+ def initial_response?; false end
28
+
29
+ def process(challenge)
30
+ digest = hmac_md5(challenge, @password)
31
+ return @user + " " + digest
32
+ ensure
33
+ @done = true
31
34
  end
32
35
 
36
+ def done?; @done end
37
+
38
+ private
39
+
33
40
  def hmac_md5(text, key)
34
41
  if key.length > 64
35
42
  key = Digest::MD5.digest(key)
@@ -47,5 +54,4 @@ class Net::IMAP::CramMD5Authenticator
47
54
  return Digest::MD5.hexdigest(k_opad + digest)
48
55
  end
49
56
 
50
- Net::IMAP.add_authenticator "CRAM-MD5", self
51
57
  end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Net::IMAP authenticator for the "`DIGEST-MD5`" SASL mechanism type, specified
4
+ # in RFC-2831[https://tools.ietf.org/html/rfc2831]. See Net::IMAP#authenticate.
5
+ #
6
+ # == Deprecated
7
+ #
8
+ # "+DIGEST-MD5+" has been deprecated by
9
+ # RFC-6331[https://tools.ietf.org/html/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
+ STAGE_ONE = :stage_one
13
+ STAGE_TWO = :stage_two
14
+ STAGE_DONE = :stage_done
15
+ private_constant :STAGE_ONE, :STAGE_TWO, :STAGE_DONE
16
+
17
+ # Authentication identity: the identity that matches the #password.
18
+ #
19
+ # RFC-2831[https://tools.ietf.org/html/rfc2831] uses the term +username+.
20
+ # "Authentication identity" is the generic term used by
21
+ # RFC-4422[https://tools.ietf.org/html/rfc4422].
22
+ # RFC-4616[https://tools.ietf.org/html/rfc4616] and many later RFCs abbreviate
23
+ # that to +authcid+. So +authcid+ is available as an alias for #username.
24
+ attr_reader :username
25
+
26
+ # A password or passphrase that matches the #username.
27
+ #
28
+ # The +password+ will be used to create the response digest.
29
+ attr_reader :password
30
+
31
+ # Authorization identity: an identity to act as or on behalf of. The identity
32
+ # form is application protocol specific. If not provided or left blank, the
33
+ # server derives an authorization identity from the authentication identity.
34
+ # The server is responsible for verifying the client's credentials and
35
+ # verifying that the identity it associates with the client's authentication
36
+ # identity is allowed to act as (or on behalf of) the authorization identity.
37
+ #
38
+ # For example, an administrator or superuser might take on another role:
39
+ #
40
+ # imap.authenticate "DIGEST-MD5", "root", ->{passwd}, authzid: "user"
41
+ #
42
+ attr_reader :authzid
43
+
44
+ # :call-seq:
45
+ # new(username, password, authzid = nil, **options) -> authenticator
46
+ # new(username:, password:, authzid: nil, **options) -> authenticator
47
+ #
48
+ # Creates an Authenticator for the "+DIGEST-MD5+" SASL mechanism.
49
+ #
50
+ # Called by Net::IMAP#authenticate and similar methods on other clients.
51
+ #
52
+ # ==== Parameters
53
+ #
54
+ # * #username — Identity whose #password is used.
55
+ # * #password — A password or passphrase associated with this #username.
56
+ # * #authzid ― Alternate identity to act as or on behalf of. Optional.
57
+ # * +warn_deprecation+ — Set to +false+ to silence the warning.
58
+ #
59
+ # See the documentation for each attribute for more details.
60
+ def initialize(user = nil, pass = nil, authz = nil,
61
+ username: nil, password: nil, authzid: nil,
62
+ warn_deprecation: true, **)
63
+ username ||= user or raise ArgumentError, "missing username"
64
+ password ||= pass or raise ArgumentError, "missing password"
65
+ authzid ||= authz
66
+ if warn_deprecation
67
+ warn "WARNING: DIGEST-MD5 SASL mechanism was deprecated by RFC6331."
68
+ # TODO: recommend SCRAM instead.
69
+ end
70
+ require "digest/md5"
71
+ require "strscan"
72
+ @username, @password, @authzid = username, password, authzid
73
+ @nc, @stage = {}, STAGE_ONE
74
+ end
75
+
76
+ def initial_response?; false end
77
+
78
+ # Responds to server challenge in two stages.
79
+ def process(challenge)
80
+ case @stage
81
+ when STAGE_ONE
82
+ @stage = STAGE_TWO
83
+ sparams = {}
84
+ c = StringScanner.new(challenge)
85
+ while c.scan(/(?:\s*,)?\s*(\w+)=("(?:[^\\"]|\\.)*"|[^,]+)\s*/)
86
+ k, v = c[1], c[2]
87
+ if v =~ /^"(.*)"$/
88
+ v = $1
89
+ if v =~ /,/
90
+ v = v.split(',')
91
+ end
92
+ end
93
+ sparams[k] = v
94
+ end
95
+
96
+ raise Net::IMAP::DataFormatError, "Bad Challenge: '#{challenge}'" unless c.eos? and sparams['qop']
97
+ raise Net::IMAP::Error, "Server does not support auth (qop = #{sparams['qop'].join(',')})" unless sparams['qop'].include?("auth")
98
+
99
+ response = {
100
+ :nonce => sparams['nonce'],
101
+ :username => @username,
102
+ :realm => sparams['realm'],
103
+ :cnonce => Digest::MD5.hexdigest("%.15f:%.15f:%d" % [Time.now.to_f, rand, Process.pid.to_s]),
104
+ :'digest-uri' => 'imap/' + sparams['realm'],
105
+ :qop => 'auth',
106
+ :maxbuf => 65535,
107
+ :nc => "%08d" % nc(sparams['nonce']),
108
+ :charset => sparams['charset'],
109
+ }
110
+
111
+ response[:authzid] = @authzid unless @authzid.nil?
112
+
113
+ # now, the real thing
114
+ a0 = Digest::MD5.digest( [ response.values_at(:username, :realm), @password ].join(':') )
115
+
116
+ a1 = [ a0, response.values_at(:nonce,:cnonce) ].join(':')
117
+ a1 << ':' + response[:authzid] unless response[:authzid].nil?
118
+
119
+ a2 = "AUTHENTICATE:" + response[:'digest-uri']
120
+ a2 << ":00000000000000000000000000000000" if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/
121
+
122
+ response[:response] = Digest::MD5.hexdigest(
123
+ [
124
+ Digest::MD5.hexdigest(a1),
125
+ response.values_at(:nonce, :nc, :cnonce, :qop),
126
+ Digest::MD5.hexdigest(a2)
127
+ ].join(':')
128
+ )
129
+
130
+ return response.keys.map {|key| qdval(key.to_s, response[key]) }.join(',')
131
+ when STAGE_TWO
132
+ @stage = STAGE_DONE
133
+ # if at the second stage, return an empty string
134
+ if challenge =~ /rspauth=/
135
+ return ''
136
+ else
137
+ raise ResponseParseError, challenge
138
+ end
139
+ else
140
+ raise ResponseParseError, challenge
141
+ end
142
+ end
143
+
144
+ def done?; @stage == STAGE_DONE end
145
+
146
+ private
147
+
148
+ def nc(nonce)
149
+ if @nc.has_key? nonce
150
+ @nc[nonce] = @nc[nonce] + 1
151
+ else
152
+ @nc[nonce] = 1
153
+ end
154
+ return @nc[nonce]
155
+ end
156
+
157
+ # some responses need quoting
158
+ def qdval(k, v)
159
+ return if k.nil? or v.nil?
160
+ if %w"username authzid realm nonce cnonce digest-uri qop".include? k
161
+ v = v.gsub(/([\\"])/, "\\\1")
162
+ return '%s="%s"' % [k, v]
163
+ else
164
+ return '%s=%s' % [k, v]
165
+ end
166
+ end
167
+
168
+ end
@@ -0,0 +1,62 @@
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://tools.ietf.org/html/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.
16
+ #
17
+ # If not explicitly provided, the server defaults to using the identity
18
+ # that was authenticated by the external credentials.
19
+ attr_reader :authzid
20
+
21
+ # :call-seq:
22
+ # new(authzid: nil, **) -> authenticator
23
+ #
24
+ # Creates an Authenticator for the "+EXTERNAL+" SASL mechanism, as
25
+ # specified in RFC-4422[https://tools.ietf.org/html/rfc4422]. To use
26
+ # this, see Net::IMAP#authenticate or your client's authentication
27
+ # method.
28
+ #
29
+ # #authzid is an optional identity to act as or on behalf of.
30
+ #
31
+ # Any other keyword parameters are quietly ignored.
32
+ def initialize(authzid: nil, **)
33
+ @authzid = authzid&.to_str&.encode "UTF-8"
34
+ if @authzid&.match?(/\u0000/u) # also validates UTF8 encoding
35
+ raise ArgumentError, "contains NULL"
36
+ end
37
+ @done = false
38
+ end
39
+
40
+ # :call-seq:
41
+ # initial_response? -> true
42
+ #
43
+ # +EXTERNAL+ can send an initial client response.
44
+ def initial_response?; true end
45
+
46
+ # Returns #authzid, or an empty string if there is no authzid.
47
+ def process(_)
48
+ authzid || ""
49
+ ensure
50
+ @done = true
51
+ end
52
+
53
+ # Returns true when the initial client response was sent.
54
+ #
55
+ # The authentication should not succeed unless this returns true, but it
56
+ # does *not* indicate success.
57
+ def done?; @done end
58
+
59
+ end
60
+ end
61
+ end
62
+ end