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.
- checksums.yaml +4 -4
- data/.github/workflows/pages.yml +46 -0
- data/.github/workflows/test.yml +12 -12
- data/Gemfile +1 -0
- data/README.md +15 -4
- data/Rakefile +0 -7
- data/benchmarks/generate_parser_benchmarks +52 -0
- data/benchmarks/parser.yml +578 -0
- data/benchmarks/stringprep.yml +1 -1
- data/lib/net/imap/authenticators.rb +26 -57
- data/lib/net/imap/command_data.rb +13 -6
- data/lib/net/imap/data_encoding.rb +3 -3
- data/lib/net/imap/deprecated_client_options.rb +139 -0
- data/lib/net/imap/response_data.rb +46 -41
- data/lib/net/imap/response_parser/parser_utils.rb +230 -0
- data/lib/net/imap/response_parser.rb +665 -627
- data/lib/net/imap/sasl/anonymous_authenticator.rb +68 -0
- data/lib/net/imap/sasl/authentication_exchange.rb +107 -0
- data/lib/net/imap/sasl/authenticators.rb +118 -0
- data/lib/net/imap/sasl/client_adapter.rb +72 -0
- data/lib/net/imap/{authenticators/cram_md5.rb → sasl/cram_md5_authenticator.rb} +15 -9
- data/lib/net/imap/sasl/digest_md5_authenticator.rb +168 -0
- data/lib/net/imap/sasl/external_authenticator.rb +62 -0
- data/lib/net/imap/sasl/gs2_header.rb +80 -0
- data/lib/net/imap/{authenticators/login.rb → sasl/login_authenticator.rb} +19 -14
- data/lib/net/imap/sasl/oauthbearer_authenticator.rb +164 -0
- data/lib/net/imap/sasl/plain_authenticator.rb +93 -0
- data/lib/net/imap/sasl/protocol_adapters.rb +45 -0
- data/lib/net/imap/sasl/scram_algorithm.rb +58 -0
- data/lib/net/imap/sasl/scram_authenticator.rb +278 -0
- data/lib/net/imap/sasl/stringprep.rb +6 -66
- data/lib/net/imap/sasl/xoauth2_authenticator.rb +88 -0
- data/lib/net/imap/sasl.rb +144 -43
- data/lib/net/imap/sasl_adapter.rb +21 -0
- data/lib/net/imap/stringprep/nameprep.rb +70 -0
- data/lib/net/imap/stringprep/saslprep.rb +69 -0
- data/lib/net/imap/stringprep/saslprep_tables.rb +96 -0
- data/lib/net/imap/stringprep/tables.rb +146 -0
- data/lib/net/imap/stringprep/trace.rb +85 -0
- data/lib/net/imap/stringprep.rb +159 -0
- data/lib/net/imap.rb +976 -590
- data/net-imap.gemspec +2 -2
- data/rakelib/saslprep.rake +4 -4
- data/rakelib/string_prep_tables_generator.rb +82 -60
- metadata +31 -12
- data/lib/net/imap/authenticators/digest_md5.rb +0 -115
- data/lib/net/imap/authenticators/plain.rb +0 -41
- data/lib/net/imap/authenticators/xoauth2.rb +0 -20
- data/lib/net/imap/sasl/saslprep.rb +0 -55
- data/lib/net/imap/sasl/saslprep_tables.rb +0 -98
- data/lib/net/imap/sasl/stringprep_tables.rb +0 -153
@@ -0,0 +1,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
|