net-imap 0.3.7 → 0.4.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/pages.yml +46 -0
- data/.github/workflows/test.yml +5 -12
- data/.gitignore +2 -0
- data/Gemfile +3 -0
- data/README.md +15 -4
- data/Rakefile +0 -7
- data/docs/styles.css +0 -12
- data/lib/net/imap/authenticators.rb +26 -57
- data/lib/net/imap/command_data.rb +13 -6
- data/lib/net/imap/data_encoding.rb +14 -2
- data/lib/net/imap/deprecated_client_options.rb +139 -0
- data/lib/net/imap/errors.rb +20 -0
- data/lib/net/imap/fetch_data.rb +518 -0
- data/lib/net/imap/response_data.rb +116 -252
- data/lib/net/imap/response_parser/parser_utils.rb +240 -0
- data/lib/net/imap/response_parser.rb +1535 -1003
- data/lib/net/imap/sasl/anonymous_authenticator.rb +69 -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} +21 -11
- data/lib/net/imap/sasl/digest_md5_authenticator.rb +180 -0
- data/lib/net/imap/sasl/external_authenticator.rb +83 -0
- data/lib/net/imap/sasl/gs2_header.rb +80 -0
- data/lib/net/imap/{authenticators/login.rb → sasl/login_authenticator.rb} +25 -16
- data/lib/net/imap/sasl/oauthbearer_authenticator.rb +199 -0
- data/lib/net/imap/sasl/plain_authenticator.rb +101 -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 +287 -0
- data/lib/net/imap/sasl/stringprep.rb +6 -66
- data/lib/net/imap/sasl/xoauth2_authenticator.rb +106 -0
- data/lib/net/imap/sasl.rb +144 -43
- data/lib/net/imap/sasl_adapter.rb +21 -0
- data/lib/net/imap/sequence_set.rb +67 -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 +1055 -612
- data/net-imap.gemspec +4 -3
- data/rakelib/benchmarks.rake +91 -0
- data/rakelib/saslprep.rake +4 -4
- data/rakelib/string_prep_tables_generator.rb +82 -60
- metadata +31 -13
- data/benchmarks/stringprep.yml +0 -65
- data/benchmarks/table-regexps.yml +0 -39
- 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,69 @@
|
|
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
|
+
# ==== Parameters
|
33
|
+
#
|
34
|
+
# * _optional_ #anonymous_message — a message to send to the server.
|
35
|
+
#
|
36
|
+
# Any other keyword arguments are silently ignored.
|
37
|
+
def initialize(anon_msg = nil, anonymous_message: nil, **)
|
38
|
+
message = (anonymous_message || anon_msg || "").to_str
|
39
|
+
@anonymous_message = StringPrep::Trace.stringprep_trace message
|
40
|
+
if (size = @anonymous_message&.length)&.> 255
|
41
|
+
raise ArgumentError,
|
42
|
+
"anonymous_message is too long. (%d codepoints)" % [size]
|
43
|
+
end
|
44
|
+
@done = false
|
45
|
+
end
|
46
|
+
|
47
|
+
# :call-seq:
|
48
|
+
# initial_response? -> true
|
49
|
+
#
|
50
|
+
# +ANONYMOUS+ can send an initial client response.
|
51
|
+
def initial_response?; true end
|
52
|
+
|
53
|
+
# Returns #anonymous_message.
|
54
|
+
def process(_server_challenge_string)
|
55
|
+
anonymous_message
|
56
|
+
ensure
|
57
|
+
@done = true
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns true when the initial client response was sent.
|
61
|
+
#
|
62
|
+
# The authentication should not succeed unless this returns true, but it
|
63
|
+
# does *not* indicate success.
|
64
|
+
def done?; @done end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
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,
|
62
|
+
# 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,22 +13,33 @@
|
|
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
|
16
|
+
class Net::IMAP::SASL::CramMD5Authenticator
|
17
|
+
def initialize(user = nil, pass = nil,
|
18
|
+
authcid: nil, username: nil,
|
19
|
+
password: nil, secret: nil,
|
20
|
+
warn_deprecation: true,
|
21
|
+
**)
|
22
|
+
if warn_deprecation
|
23
|
+
warn "WARNING: CRAM-MD5 mechanism is deprecated." # TODO: recommend SCRAM
|
24
|
+
end
|
25
|
+
require "digest/md5"
|
26
|
+
@user = authcid || username || user
|
27
|
+
@password = password || secret || pass
|
28
|
+
@done = false
|
29
|
+
end
|
30
|
+
|
31
|
+
def initial_response?; false end
|
32
|
+
|
17
33
|
def process(challenge)
|
18
34
|
digest = hmac_md5(challenge, @password)
|
19
35
|
return @user + " " + digest
|
36
|
+
ensure
|
37
|
+
@done = true
|
20
38
|
end
|
21
39
|
|
22
|
-
|
40
|
+
def done?; @done end
|
23
41
|
|
24
|
-
|
25
|
-
if warn_deprecation
|
26
|
-
warn "WARNING: CRAM-MD5 mechanism is deprecated." # TODO: recommend SCRAM
|
27
|
-
end
|
28
|
-
require "digest/md5"
|
29
|
-
@user = user
|
30
|
-
@password = password
|
31
|
-
end
|
42
|
+
private
|
32
43
|
|
33
44
|
def hmac_md5(text, key)
|
34
45
|
if key.length > 64
|
@@ -47,5 +58,4 @@ class Net::IMAP::CramMD5Authenticator
|
|
47
58
|
return Digest::MD5.hexdigest(k_opad + digest)
|
48
59
|
end
|
49
60
|
|
50
|
-
Net::IMAP.add_authenticator "CRAM-MD5", self
|
51
61
|
end
|
@@ -0,0 +1,180 @@
|
|
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
|
+
# this to +authcid+.
|
24
|
+
attr_reader :username
|
25
|
+
alias authcid username
|
26
|
+
|
27
|
+
# A password or passphrase that matches the #username.
|
28
|
+
#
|
29
|
+
# The +password+ will be used to create the response digest.
|
30
|
+
attr_reader :password
|
31
|
+
|
32
|
+
# Authorization identity: an identity to act as or on behalf of. The identity
|
33
|
+
# form is application protocol specific. If not provided or left blank, the
|
34
|
+
# server derives an authorization identity from the authentication identity.
|
35
|
+
# The server is responsible for verifying the client's credentials and
|
36
|
+
# verifying that the identity it associates with the client's authentication
|
37
|
+
# identity is allowed to act as (or on behalf of) the authorization identity.
|
38
|
+
#
|
39
|
+
# For example, an administrator or superuser might take on another role:
|
40
|
+
#
|
41
|
+
# imap.authenticate "DIGEST-MD5", "root", ->{passwd}, authzid: "user"
|
42
|
+
#
|
43
|
+
attr_reader :authzid
|
44
|
+
|
45
|
+
# :call-seq:
|
46
|
+
# new(username, password, authzid = nil, **options) -> authenticator
|
47
|
+
# new(username:, password:, authzid: nil, **options) -> authenticator
|
48
|
+
# new(authcid:, password:, authzid: nil, **options) -> authenticator
|
49
|
+
#
|
50
|
+
# Creates an Authenticator for the "+DIGEST-MD5+" SASL mechanism.
|
51
|
+
#
|
52
|
+
# Called by Net::IMAP#authenticate and similar methods on other clients.
|
53
|
+
#
|
54
|
+
# ==== Parameters
|
55
|
+
#
|
56
|
+
# * #authcid ― Authentication identity that is associated with #password.
|
57
|
+
#
|
58
|
+
# #username ― An alias for +authcid+.
|
59
|
+
#
|
60
|
+
# * #password ― A password or passphrase associated with this #authcid.
|
61
|
+
#
|
62
|
+
# * _optional_ #authzid ― Authorization identity to act as or on behalf of.
|
63
|
+
#
|
64
|
+
# When +authzid+ is not set, the server should derive the authorization
|
65
|
+
# identity from the authentication identity.
|
66
|
+
#
|
67
|
+
# * _optional_ +warn_deprecation+ — Set to +false+ to silence the warning.
|
68
|
+
#
|
69
|
+
# Any other keyword arguments are silently ignored.
|
70
|
+
def initialize(user = nil, pass = nil, authz = nil,
|
71
|
+
username: nil, password: nil, authzid: nil,
|
72
|
+
authcid: nil, secret: nil,
|
73
|
+
warn_deprecation: true, **)
|
74
|
+
username = authcid || username || user or
|
75
|
+
raise ArgumentError, "missing username (authcid)"
|
76
|
+
password ||= secret || pass or raise ArgumentError, "missing password"
|
77
|
+
authzid ||= authz
|
78
|
+
if warn_deprecation
|
79
|
+
warn "WARNING: DIGEST-MD5 SASL mechanism was deprecated by RFC6331."
|
80
|
+
# TODO: recommend SCRAM instead.
|
81
|
+
end
|
82
|
+
require "digest/md5"
|
83
|
+
require "strscan"
|
84
|
+
@username, @password, @authzid = username, password, authzid
|
85
|
+
@nc, @stage = {}, STAGE_ONE
|
86
|
+
end
|
87
|
+
|
88
|
+
def initial_response?; false end
|
89
|
+
|
90
|
+
# Responds to server challenge in two stages.
|
91
|
+
def process(challenge)
|
92
|
+
case @stage
|
93
|
+
when STAGE_ONE
|
94
|
+
@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
|
107
|
+
|
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")
|
110
|
+
|
111
|
+
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'],
|
121
|
+
}
|
122
|
+
|
123
|
+
response[:authzid] = @authzid unless @authzid.nil?
|
124
|
+
|
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(',')
|
143
|
+
when STAGE_TWO
|
144
|
+
@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
|
151
|
+
else
|
152
|
+
raise ResponseParseError, challenge
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def done?; @stage == STAGE_DONE end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
def nc(nonce)
|
161
|
+
if @nc.has_key? nonce
|
162
|
+
@nc[nonce] = @nc[nonce] + 1
|
163
|
+
else
|
164
|
+
@nc[nonce] = 1
|
165
|
+
end
|
166
|
+
return @nc[nonce]
|
167
|
+
end
|
168
|
+
|
169
|
+
# some responses need quoting
|
170
|
+
def qdval(k, v)
|
171
|
+
return if k.nil? or v.nil?
|
172
|
+
if %w"username authzid realm nonce cnonce digest-uri qop".include? k
|
173
|
+
v = v.gsub(/([\\"])/, "\\\1")
|
174
|
+
return '%s="%s"' % [k, v]
|
175
|
+
else
|
176
|
+
return '%s=%s' % [k, v]
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
@@ -0,0 +1,83 @@
|
|
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. The
|
16
|
+
# identity form is application protocol specific. If not provided or
|
17
|
+
# left blank, the server derives an authorization identity from the
|
18
|
+
# authentication identity. The server is responsible for verifying the
|
19
|
+
# client's credentials and verifying that the identity it associates
|
20
|
+
# with the client's authentication identity is allowed to act as (or on
|
21
|
+
# behalf of) the authorization identity.
|
22
|
+
#
|
23
|
+
# For example, an administrator or superuser might take on another role:
|
24
|
+
#
|
25
|
+
# imap.authenticate "PLAIN", "root", passwd, authzid: "user"
|
26
|
+
#
|
27
|
+
attr_reader :authzid
|
28
|
+
alias username authzid
|
29
|
+
|
30
|
+
# :call-seq:
|
31
|
+
# new(authzid: nil, **) -> authenticator
|
32
|
+
# new(username: nil, **) -> authenticator
|
33
|
+
# new(username = nil, **) -> authenticator
|
34
|
+
#
|
35
|
+
# Creates an Authenticator for the "+EXTERNAL+" SASL mechanism, as
|
36
|
+
# specified in RFC-4422[https://tools.ietf.org/html/rfc4422]. To use
|
37
|
+
# this, see Net::IMAP#authenticate or your client's authentication
|
38
|
+
# method.
|
39
|
+
#
|
40
|
+
# ==== Parameters
|
41
|
+
#
|
42
|
+
# * _optional_ #authzid ― Authorization identity to act as or on behalf of.
|
43
|
+
#
|
44
|
+
# _optional_ #username ― An alias for #authzid.
|
45
|
+
#
|
46
|
+
# Note that, unlike some other authenticators, +username+ sets the
|
47
|
+
# _authorization_ identity and not the _authentication_ identity. The
|
48
|
+
# authentication identity is established for the client by the
|
49
|
+
# external credentials.
|
50
|
+
#
|
51
|
+
# Any other keyword parameters are quietly ignored.
|
52
|
+
def initialize(user = nil, authzid: nil, username: nil, **)
|
53
|
+
authzid ||= username || user
|
54
|
+
@authzid = authzid&.to_str&.encode "UTF-8"
|
55
|
+
if @authzid&.match?(/\u0000/u) # also validates UTF8 encoding
|
56
|
+
raise ArgumentError, "contains NULL"
|
57
|
+
end
|
58
|
+
@done = false
|
59
|
+
end
|
60
|
+
|
61
|
+
# :call-seq:
|
62
|
+
# initial_response? -> true
|
63
|
+
#
|
64
|
+
# +EXTERNAL+ can send an initial client response.
|
65
|
+
def initial_response?; true end
|
66
|
+
|
67
|
+
# Returns #authzid, or an empty string if there is no authzid.
|
68
|
+
def process(_)
|
69
|
+
authzid || ""
|
70
|
+
ensure
|
71
|
+
@done = true
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns true when the initial client response was sent.
|
75
|
+
#
|
76
|
+
# The authentication should not succeed unless this returns true, but it
|
77
|
+
# does *not* indicate success.
|
78
|
+
def done?; @done end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|