net-imap 0.3.4 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
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