summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authornicholas a. evans <[email protected]>2021-04-27 16:33:27 -0400
committerHiroshi SHIBATA <[email protected]>2021-05-06 15:20:33 +0900
commit331005812fc288fb27bef542ecfbb2c061d86999 (patch)
tree4c112a38b2a87436b9cbabfcf72f38d10436c892
parenta7f7479872cc70678b5f8374347c509945c04e82 (diff)
[ruby/net-imap] Move each authenticator to its own file
Also updates rdoc with SASL specifications and deprecations. Of these four, only `PLAIN` isn't deprecated! +@@authenticators+ was changed to a class instance var +@authenticators+. No one should have been using the class variable directly, so that should be fine. https://github.com/ruby/net-imap/commit/23f241b081
-rw-r--r--lib/net/imap.rb206
-rw-r--r--lib/net/imap/authenticators.rb44
-rw-r--r--lib/net/imap/authenticators/cram_md5.rb47
-rw-r--r--lib/net/imap/authenticators/digest_md5.rb111
-rw-r--r--lib/net/imap/authenticators/login.rb34
-rw-r--r--lib/net/imap/authenticators/plain.rb19
6 files changed, 257 insertions, 204 deletions
diff --git a/lib/net/imap.rb b/lib/net/imap.rb
index d3f2e25aeb..8a6b295b75 100644
--- a/lib/net/imap.rb
+++ b/lib/net/imap.rb
@@ -16,8 +16,6 @@
require "socket"
require "monitor"
-require "digest/md5"
-require "strscan"
require 'net/protocol'
begin
require "openssl"
@@ -292,31 +290,6 @@ module Net
@@max_flag_count = count
end
- # Adds an authenticator for Net::IMAP#authenticate. +auth_type+
- # is the type of authentication this authenticator supports
- # (for instance, "LOGIN"). The +authenticator+ is an object
- # which defines a process() method to handle authentication with
- # the server. See Net::IMAP::LoginAuthenticator,
- # Net::IMAP::CramMD5Authenticator, and Net::IMAP::DigestMD5Authenticator
- # for examples.
- #
- #
- # If +auth_type+ refers to an existing authenticator, it will be
- # replaced by the new one.
- def self.add_authenticator(auth_type, authenticator)
- @@authenticators[auth_type] = authenticator
- end
-
- # Builds an authenticator for Net::IMAP#authenticate.
- def self.authenticator(auth_type, *args)
- auth_type = auth_type.upcase
- unless @@authenticators.has_key?(auth_type)
- raise ArgumentError,
- format('unknown auth type - "%s"', auth_type)
- end
- @@authenticators[auth_type].new(*args)
- end
-
# The default port for IMAP connections, port 143
def self.default_port
return PORT
@@ -1124,7 +1097,6 @@ module Net
SSL_PORT = 993 # :nodoc:
@@debug = false
- @@authenticators = {}
@@max_flag_count = 10000
# :call-seq:
@@ -3901,182 +3873,6 @@ module Net
end
end
- # Authenticator for the "LOGIN" authentication type. See
- # #authenticate().
- class LoginAuthenticator
- def process(data)
- case @state
- when STATE_USER
- @state = STATE_PASSWORD
- return @user
- when STATE_PASSWORD
- return @password
- end
- end
-
- private
-
- STATE_USER = :USER
- STATE_PASSWORD = :PASSWORD
-
- def initialize(user, password)
- @user = user
- @password = password
- @state = STATE_USER
- end
- end
- add_authenticator "LOGIN", LoginAuthenticator
-
- # Authenticator for the "PLAIN" authentication type. See
- # #authenticate().
- class PlainAuthenticator
- def process(data)
- return "\0#{@user}\0#{@password}"
- end
-
- private
-
- def initialize(user, password)
- @user = user
- @password = password
- end
- end
- add_authenticator "PLAIN", PlainAuthenticator
-
- # Authenticator for the "CRAM-MD5" authentication type. See
- # #authenticate().
- class CramMD5Authenticator
- def process(challenge)
- digest = hmac_md5(challenge, @password)
- return @user + " " + digest
- end
-
- private
-
- def initialize(user, password)
- @user = user
- @password = password
- end
-
- def hmac_md5(text, key)
- if key.length > 64
- key = Digest::MD5.digest(key)
- end
-
- k_ipad = key + "\0" * (64 - key.length)
- k_opad = key + "\0" * (64 - key.length)
- for i in 0..63
- k_ipad[i] = (k_ipad[i].ord ^ 0x36).chr
- k_opad[i] = (k_opad[i].ord ^ 0x5c).chr
- end
-
- digest = Digest::MD5.digest(k_ipad + text)
-
- return Digest::MD5.hexdigest(k_opad + digest)
- end
- end
- add_authenticator "CRAM-MD5", CramMD5Authenticator
-
- # Authenticator for the "DIGEST-MD5" authentication type. See
- # #authenticate().
- class DigestMD5Authenticator
- def process(challenge)
- case @stage
- when STAGE_ONE
- @stage = STAGE_TWO
- sparams = {}
- c = StringScanner.new(challenge)
- while c.scan(/(?:\s*,)?\s*(\w+)=("(?:[^\\"]+|\\.)*"|[^,]+)\s*/)
- k, v = c[1], c[2]
- if v =~ /^"(.*)"$/
- v = $1
- if v =~ /,/
- v = v.split(',')
- end
- end
- sparams[k] = v
- end
-
- raise DataFormatError, "Bad Challenge: '#{challenge}'" unless c.rest.size == 0
- raise Error, "Server does not support auth (qop = #{sparams['qop'].join(',')})" unless sparams['qop'].include?("auth")
-
- response = {
- :nonce => sparams['nonce'],
- :username => @user,
- :realm => sparams['realm'],
- :cnonce => Digest::MD5.hexdigest("%.15f:%.15f:%d" % [Time.now.to_f, rand, Process.pid.to_s]),
- :'digest-uri' => 'imap/' + sparams['realm'],
- :qop => 'auth',
- :maxbuf => 65535,
- :nc => "%08d" % nc(sparams['nonce']),
- :charset => sparams['charset'],
- }
-
- response[:authzid] = @authname unless @authname.nil?
-
- # now, the real thing
- a0 = Digest::MD5.digest( [ response.values_at(:username, :realm), @password ].join(':') )
-
- a1 = [ a0, response.values_at(:nonce,:cnonce) ].join(':')
- a1 << ':' + response[:authzid] unless response[:authzid].nil?
-
- a2 = "AUTHENTICATE:" + response[:'digest-uri']
- a2 << ":00000000000000000000000000000000" if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/
-
- response[:response] = Digest::MD5.hexdigest(
- [
- Digest::MD5.hexdigest(a1),
- response.values_at(:nonce, :nc, :cnonce, :qop),
- Digest::MD5.hexdigest(a2)
- ].join(':')
- )
-
- return response.keys.map {|key| qdval(key.to_s, response[key]) }.join(',')
- when STAGE_TWO
- @stage = nil
- # if at the second stage, return an empty string
- if challenge =~ /rspauth=/
- return ''
- else
- raise ResponseParseError, challenge
- end
- else
- raise ResponseParseError, challenge
- end
- end
-
- def initialize(user, password, authname = nil)
- @user, @password, @authname = user, password, authname
- @nc, @stage = {}, STAGE_ONE
- end
-
- private
-
- STAGE_ONE = :stage_one
- STAGE_TWO = :stage_two
-
- def nc(nonce)
- if @nc.has_key? nonce
- @nc[nonce] = @nc[nonce] + 1
- else
- @nc[nonce] = 1
- end
- return @nc[nonce]
- end
-
- # some responses need quoting
- def qdval(k, v)
- return if k.nil? or v.nil?
- if %w"username authzid realm nonce cnonce digest-uri qop".include? k
- v.gsub!(/([\\"])/, "\\\1")
- return '%s="%s"' % [k, v]
- else
- return '%s=%s' % [k, v]
- end
- end
- end
- add_authenticator "DIGEST-MD5", DigestMD5Authenticator
-
# Superclass of IMAP errors.
class Error < StandardError
end
@@ -4130,3 +3926,5 @@ module Net
end
end
end
+
+require_relative "imap/authenticators"
diff --git a/lib/net/imap/authenticators.rb b/lib/net/imap/authenticators.rb
new file mode 100644
index 0000000000..f86b77b09f
--- /dev/null
+++ b/lib/net/imap/authenticators.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+# Registry for SASL authenticators used by Net::IMAP.
+module Net::IMAP::Authenticators
+
+ # Adds an authenticator for Net::IMAP#authenticate. +auth_type+ is the
+ # {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
+ # supported by +authenticator+ (for instance, "+LOGIN+"). The +authenticator+
+ # is an object which defines a +#process+ method to handle authentication with
+ # the server. See Net::IMAP::LoginAuthenticator,
+ # Net::IMAP::CramMD5Authenticator, and Net::IMAP::DigestMD5Authenticator for
+ # examples.
+ #
+ # If +auth_type+ refers to an existing authenticator, it will be
+ # replaced by the new one.
+ def add_authenticator(auth_type, authenticator)
+ authenticators[auth_type] = authenticator
+ end
+
+ # Builds an authenticator for Net::IMAP#authenticate. +args+ will be passed
+ # directly to the chosen authenticator's +#initialize+.
+ def authenticator(auth_type, *args)
+ auth_type = auth_type.upcase
+ unless authenticators.has_key?(auth_type)
+ raise ArgumentError,
+ format('unknown auth type - "%s"', auth_type)
+ end
+ authenticators[auth_type].new(*args)
+ end
+
+ private
+
+ def authenticators
+ @authenticators ||= {}
+ end
+
+end
+
+Net::IMAP.extend Net::IMAP::Authenticators
+
+require_relative "authenticators/login"
+require_relative "authenticators/plain"
+require_relative "authenticators/cram_md5"
+require_relative "authenticators/digest_md5"
diff --git a/lib/net/imap/authenticators/cram_md5.rb b/lib/net/imap/authenticators/cram_md5.rb
new file mode 100644
index 0000000000..0bef638185
--- /dev/null
+++ b/lib/net/imap/authenticators/cram_md5.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require "digest/md5"
+
+# Authenticator for the "+CRAM-MD5+" SASL mechanism. See
+# Net::IMAP#authenticate.
+#
+# == Deprecated
+#
+# +CRAM-MD5+ should be considered obsolete and insecure. It is included for
+# backward compatibility with historic servers.
+# {draft-ietf-sasl-crammd5-to-historic}[https://tools.ietf.org/html/draft-ietf-sasl-crammd5-to-historic-00.html]
+# recommends using +SCRAM-*+ or +PLAIN+ protected by TLS instead. Additionally,
+# RFC8314[https://tools.ietf.org/html/rfc8314] discourage the use of cleartext
+# and recommends TLS version 1.2 or greater be used for all traffic.
+class Net::IMAP::CramMD5Authenticator
+ def process(challenge)
+ digest = hmac_md5(challenge, @password)
+ return @user + " " + digest
+ end
+
+ private
+
+ def initialize(user, password)
+ @user = user
+ @password = password
+ end
+
+ def hmac_md5(text, key)
+ if key.length > 64
+ key = Digest::MD5.digest(key)
+ end
+
+ k_ipad = key + "\0" * (64 - key.length)
+ k_opad = key + "\0" * (64 - key.length)
+ for i in 0..63
+ k_ipad[i] = (k_ipad[i].ord ^ 0x36).chr
+ k_opad[i] = (k_opad[i].ord ^ 0x5c).chr
+ end
+
+ digest = Digest::MD5.digest(k_ipad + text)
+
+ return Digest::MD5.hexdigest(k_opad + digest)
+ end
+
+ Net::IMAP.add_authenticator "PLAIN", self
+end
diff --git a/lib/net/imap/authenticators/digest_md5.rb b/lib/net/imap/authenticators/digest_md5.rb
new file mode 100644
index 0000000000..a5f4b9093e
--- /dev/null
+++ b/lib/net/imap/authenticators/digest_md5.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+require "digest/md5"
+require "strscan"
+
+# Net::IMAP authenticator for the "`DIGEST-MD5`" SASL mechanism type. See
+# Net::IMAP#authenticate.
+#
+# == Deprecated
+#
+# "+DIGEST-MD5+" has been deprecated by
+# {RFC6331}[https://tools.ietf.org/html/rfc6331] and should not be used. It
+# is included for backward compatibility with historic servers.
+class Net::IMAP::DigestMD5Authenticator
+ def process(challenge)
+ case @stage
+ when STAGE_ONE
+ @stage = STAGE_TWO
+ sparams = {}
+ c = StringScanner.new(challenge)
+ while c.scan(/(?:\s*,)?\s*(\w+)=("(?:[^\\"]+|\\.)*"|[^,]+)\s*/)
+ k, v = c[1], c[2]
+ if v =~ /^"(.*)"$/
+ v = $1
+ if v =~ /,/
+ v = v.split(',')
+ end
+ end
+ sparams[k] = v
+ end
+
+ raise DataFormatError, "Bad Challenge: '#{challenge}'" unless c.rest.size == 0
+ raise Error, "Server does not support auth (qop = #{sparams['qop'].join(',')})" unless sparams['qop'].include?("auth")
+
+ response = {
+ :nonce => sparams['nonce'],
+ :username => @user,
+ :realm => sparams['realm'],
+ :cnonce => Digest::MD5.hexdigest("%.15f:%.15f:%d" % [Time.now.to_f, rand, Process.pid.to_s]),
+ :'digest-uri' => 'imap/' + sparams['realm'],
+ :qop => 'auth',
+ :maxbuf => 65535,
+ :nc => "%08d" % nc(sparams['nonce']),
+ :charset => sparams['charset'],
+ }
+
+ response[:authzid] = @authname unless @authname.nil?
+
+ # now, the real thing
+ a0 = Digest::MD5.digest( [ response.values_at(:username, :realm), @password ].join(':') )
+
+ a1 = [ a0, response.values_at(:nonce,:cnonce) ].join(':')
+ a1 << ':' + response[:authzid] unless response[:authzid].nil?
+
+ a2 = "AUTHENTICATE:" + response[:'digest-uri']
+ a2 << ":00000000000000000000000000000000" if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/
+
+ response[:response] = Digest::MD5.hexdigest(
+ [
+ Digest::MD5.hexdigest(a1),
+ response.values_at(:nonce, :nc, :cnonce, :qop),
+ Digest::MD5.hexdigest(a2)
+ ].join(':')
+ )
+
+ return response.keys.map {|key| qdval(key.to_s, response[key]) }.join(',')
+ when STAGE_TWO
+ @stage = nil
+ # if at the second stage, return an empty string
+ if challenge =~ /rspauth=/
+ return ''
+ else
+ raise ResponseParseError, challenge
+ end
+ else
+ raise ResponseParseError, challenge
+ end
+ end
+
+ def initialize(user, password, authname = nil)
+ @user, @password, @authname = user, password, authname
+ @nc, @stage = {}, STAGE_ONE
+ end
+
+ private
+
+ STAGE_ONE = :stage_one
+ STAGE_TWO = :stage_two
+
+ def nc(nonce)
+ if @nc.has_key? nonce
+ @nc[nonce] = @nc[nonce] + 1
+ else
+ @nc[nonce] = 1
+ end
+ return @nc[nonce]
+ end
+
+ # some responses need quoting
+ def qdval(k, v)
+ return if k.nil? or v.nil?
+ if %w"username authzid realm nonce cnonce digest-uri qop".include? k
+ v.gsub!(/([\\"])/, "\\\1")
+ return '%s="%s"' % [k, v]
+ else
+ return '%s=%s' % [k, v]
+ end
+ end
+
+ Net::IMAP.add_authenticator "DIGEST-MD5", self
+end
diff --git a/lib/net/imap/authenticators/login.rb b/lib/net/imap/authenticators/login.rb
new file mode 100644
index 0000000000..8925d6de62
--- /dev/null
+++ b/lib/net/imap/authenticators/login.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+# Authenticator for the "+LOGIN+" SASL mechanism. See Net::IMAP#authenticate.
+#
+# == Deprecated
+#
+# The {SASL mechanisms
+# registry}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
+# marks "LOGIN" as obsoleted in favor of "PLAIN". See also
+# {draft-murchison-sasl-login}[https://www.iana.org/go/draft-murchison-sasl-login].
+class Net::IMAP::LoginAuthenticator
+ def process(data)
+ case @state
+ when STATE_USER
+ @state = STATE_PASSWORD
+ return @user
+ when STATE_PASSWORD
+ return @password
+ end
+ end
+
+ private
+
+ STATE_USER = :USER
+ STATE_PASSWORD = :PASSWORD
+
+ def initialize(user, password)
+ @user = user
+ @password = password
+ @state = STATE_USER
+ end
+
+ Net::IMAP.add_authenticator "LOGIN", self
+end
diff --git a/lib/net/imap/authenticators/plain.rb b/lib/net/imap/authenticators/plain.rb
new file mode 100644
index 0000000000..0829476c51
--- /dev/null
+++ b/lib/net/imap/authenticators/plain.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+# Authenticator for the "+PLAIN+" SASL mechanism. See Net::IMAP#authenticate.
+#
+# See RFC4616[https://tools.ietf.org/html/rfc4616] for the specification.
+class Net::IMAP::PlainAuthenticator
+ def process(data)
+ return "\0#{@user}\0#{@password}"
+ end
+
+ private
+
+ def initialize(user, password)
+ @user = user
+ @password = password
+ end
+
+ Net::IMAP.add_authenticator "PLAIN", self
+end