Skip to content

Commit 23f241b

Browse files
committed
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.
1 parent fcd8f26 commit 23f241b

File tree

6 files changed

+257
-204
lines changed

6 files changed

+257
-204
lines changed

Diff for: lib/net/imap.rb

+2-204
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616

1717
require "socket"
1818
require "monitor"
19-
require "digest/md5"
20-
require "strscan"
2119
require 'net/protocol'
2220
begin
2321
require "openssl"
@@ -292,31 +290,6 @@ def self.max_flag_count=(count)
292290
@@max_flag_count = count
293291
end
294292

295-
# Adds an authenticator for Net::IMAP#authenticate. +auth_type+
296-
# is the type of authentication this authenticator supports
297-
# (for instance, "LOGIN"). The +authenticator+ is an object
298-
# which defines a process() method to handle authentication with
299-
# the server. See Net::IMAP::LoginAuthenticator,
300-
# Net::IMAP::CramMD5Authenticator, and Net::IMAP::DigestMD5Authenticator
301-
# for examples.
302-
#
303-
#
304-
# If +auth_type+ refers to an existing authenticator, it will be
305-
# replaced by the new one.
306-
def self.add_authenticator(auth_type, authenticator)
307-
@@authenticators[auth_type] = authenticator
308-
end
309-
310-
# Builds an authenticator for Net::IMAP#authenticate.
311-
def self.authenticator(auth_type, *args)
312-
auth_type = auth_type.upcase
313-
unless @@authenticators.has_key?(auth_type)
314-
raise ArgumentError,
315-
format('unknown auth type - "%s"', auth_type)
316-
end
317-
@@authenticators[auth_type].new(*args)
318-
end
319-
320293
# The default port for IMAP connections, port 143
321294
def self.default_port
322295
return PORT
@@ -1124,7 +1097,6 @@ def self.format_datetime(time)
11241097
SSL_PORT = 993 # :nodoc:
11251098

11261099
@@debug = false
1127-
@@authenticators = {}
11281100
@@max_flag_count = 10000
11291101

11301102
# :call-seq:
@@ -3901,182 +3873,6 @@ def parse_error(fmt, *args)
39013873
end
39023874
end
39033875

3904-
# Authenticator for the "LOGIN" authentication type. See
3905-
# #authenticate().
3906-
class LoginAuthenticator
3907-
def process(data)
3908-
case @state
3909-
when STATE_USER
3910-
@state = STATE_PASSWORD
3911-
return @user
3912-
when STATE_PASSWORD
3913-
return @password
3914-
end
3915-
end
3916-
3917-
private
3918-
3919-
STATE_USER = :USER
3920-
STATE_PASSWORD = :PASSWORD
3921-
3922-
def initialize(user, password)
3923-
@user = user
3924-
@password = password
3925-
@state = STATE_USER
3926-
end
3927-
end
3928-
add_authenticator "LOGIN", LoginAuthenticator
3929-
3930-
# Authenticator for the "PLAIN" authentication type. See
3931-
# #authenticate().
3932-
class PlainAuthenticator
3933-
def process(data)
3934-
return "\0#{@user}\0#{@password}"
3935-
end
3936-
3937-
private
3938-
3939-
def initialize(user, password)
3940-
@user = user
3941-
@password = password
3942-
end
3943-
end
3944-
add_authenticator "PLAIN", PlainAuthenticator
3945-
3946-
# Authenticator for the "CRAM-MD5" authentication type. See
3947-
# #authenticate().
3948-
class CramMD5Authenticator
3949-
def process(challenge)
3950-
digest = hmac_md5(challenge, @password)
3951-
return @user + " " + digest
3952-
end
3953-
3954-
private
3955-
3956-
def initialize(user, password)
3957-
@user = user
3958-
@password = password
3959-
end
3960-
3961-
def hmac_md5(text, key)
3962-
if key.length > 64
3963-
key = Digest::MD5.digest(key)
3964-
end
3965-
3966-
k_ipad = key + "\0" * (64 - key.length)
3967-
k_opad = key + "\0" * (64 - key.length)
3968-
for i in 0..63
3969-
k_ipad[i] = (k_ipad[i].ord ^ 0x36).chr
3970-
k_opad[i] = (k_opad[i].ord ^ 0x5c).chr
3971-
end
3972-
3973-
digest = Digest::MD5.digest(k_ipad + text)
3974-
3975-
return Digest::MD5.hexdigest(k_opad + digest)
3976-
end
3977-
end
3978-
add_authenticator "CRAM-MD5", CramMD5Authenticator
3979-
3980-
# Authenticator for the "DIGEST-MD5" authentication type. See
3981-
# #authenticate().
3982-
class DigestMD5Authenticator
3983-
def process(challenge)
3984-
case @stage
3985-
when STAGE_ONE
3986-
@stage = STAGE_TWO
3987-
sparams = {}
3988-
c = StringScanner.new(challenge)
3989-
while c.scan(/(?:\s*,)?\s*(\w+)=("(?:[^\\"]+|\\.)*"|[^,]+)\s*/)
3990-
k, v = c[1], c[2]
3991-
if v =~ /^"(.*)"$/
3992-
v = $1
3993-
if v =~ /,/
3994-
v = v.split(',')
3995-
end
3996-
end
3997-
sparams[k] = v
3998-
end
3999-
4000-
raise DataFormatError, "Bad Challenge: '#{challenge}'" unless c.rest.size == 0
4001-
raise Error, "Server does not support auth (qop = #{sparams['qop'].join(',')})" unless sparams['qop'].include?("auth")
4002-
4003-
response = {
4004-
:nonce => sparams['nonce'],
4005-
:username => @user,
4006-
:realm => sparams['realm'],
4007-
:cnonce => Digest::MD5.hexdigest("%.15f:%.15f:%d" % [Time.now.to_f, rand, Process.pid.to_s]),
4008-
:'digest-uri' => 'imap/' + sparams['realm'],
4009-
:qop => 'auth',
4010-
:maxbuf => 65535,
4011-
:nc => "%08d" % nc(sparams['nonce']),
4012-
:charset => sparams['charset'],
4013-
}
4014-
4015-
response[:authzid] = @authname unless @authname.nil?
4016-
4017-
# now, the real thing
4018-
a0 = Digest::MD5.digest( [ response.values_at(:username, :realm), @password ].join(':') )
4019-
4020-
a1 = [ a0, response.values_at(:nonce,:cnonce) ].join(':')
4021-
a1 << ':' + response[:authzid] unless response[:authzid].nil?
4022-
4023-
a2 = "AUTHENTICATE:" + response[:'digest-uri']
4024-
a2 << ":00000000000000000000000000000000" if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/
4025-
4026-
response[:response] = Digest::MD5.hexdigest(
4027-
[
4028-
Digest::MD5.hexdigest(a1),
4029-
response.values_at(:nonce, :nc, :cnonce, :qop),
4030-
Digest::MD5.hexdigest(a2)
4031-
].join(':')
4032-
)
4033-
4034-
return response.keys.map {|key| qdval(key.to_s, response[key]) }.join(',')
4035-
when STAGE_TWO
4036-
@stage = nil
4037-
# if at the second stage, return an empty string
4038-
if challenge =~ /rspauth=/
4039-
return ''
4040-
else
4041-
raise ResponseParseError, challenge
4042-
end
4043-
else
4044-
raise ResponseParseError, challenge
4045-
end
4046-
end
4047-
4048-
def initialize(user, password, authname = nil)
4049-
@user, @password, @authname = user, password, authname
4050-
@nc, @stage = {}, STAGE_ONE
4051-
end
4052-
4053-
private
4054-
4055-
STAGE_ONE = :stage_one
4056-
STAGE_TWO = :stage_two
4057-
4058-
def nc(nonce)
4059-
if @nc.has_key? nonce
4060-
@nc[nonce] = @nc[nonce] + 1
4061-
else
4062-
@nc[nonce] = 1
4063-
end
4064-
return @nc[nonce]
4065-
end
4066-
4067-
# some responses need quoting
4068-
def qdval(k, v)
4069-
return if k.nil? or v.nil?
4070-
if %w"username authzid realm nonce cnonce digest-uri qop".include? k
4071-
v.gsub!(/([\\"])/, "\\\1")
4072-
return '%s="%s"' % [k, v]
4073-
else
4074-
return '%s=%s' % [k, v]
4075-
end
4076-
end
4077-
end
4078-
add_authenticator "DIGEST-MD5", DigestMD5Authenticator
4079-
40803876
# Superclass of IMAP errors.
40813877
class Error < StandardError
40823878
end
@@ -4130,3 +3926,5 @@ class FlagCountError < Error
41303926
end
41313927
end
41323928
end
3929+
3930+
require_relative "imap/authenticators"

Diff for: lib/net/imap/authenticators.rb

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# frozen_string_literal: true
2+
3+
# Registry for SASL authenticators used by Net::IMAP.
4+
module Net::IMAP::Authenticators
5+
6+
# Adds an authenticator for Net::IMAP#authenticate. +auth_type+ is the
7+
# {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
8+
# supported by +authenticator+ (for instance, "+LOGIN+"). The +authenticator+
9+
# is an object which defines a +#process+ method to handle authentication with
10+
# the server. See Net::IMAP::LoginAuthenticator,
11+
# Net::IMAP::CramMD5Authenticator, and Net::IMAP::DigestMD5Authenticator for
12+
# examples.
13+
#
14+
# If +auth_type+ refers to an existing authenticator, it will be
15+
# replaced by the new one.
16+
def add_authenticator(auth_type, authenticator)
17+
authenticators[auth_type] = authenticator
18+
end
19+
20+
# Builds an authenticator for Net::IMAP#authenticate. +args+ will be passed
21+
# directly to the chosen authenticator's +#initialize+.
22+
def authenticator(auth_type, *args)
23+
auth_type = auth_type.upcase
24+
unless authenticators.has_key?(auth_type)
25+
raise ArgumentError,
26+
format('unknown auth type - "%s"', auth_type)
27+
end
28+
authenticators[auth_type].new(*args)
29+
end
30+
31+
private
32+
33+
def authenticators
34+
@authenticators ||= {}
35+
end
36+
37+
end
38+
39+
Net::IMAP.extend Net::IMAP::Authenticators
40+
41+
require_relative "authenticators/login"
42+
require_relative "authenticators/plain"
43+
require_relative "authenticators/cram_md5"
44+
require_relative "authenticators/digest_md5"

Diff for: lib/net/imap/authenticators/cram_md5.rb

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# frozen_string_literal: true
2+
3+
require "digest/md5"
4+
5+
# Authenticator for the "+CRAM-MD5+" SASL mechanism. See
6+
# Net::IMAP#authenticate.
7+
#
8+
# == Deprecated
9+
#
10+
# +CRAM-MD5+ should be considered obsolete and insecure. It is included for
11+
# backward compatibility with historic servers.
12+
# {draft-ietf-sasl-crammd5-to-historic}[https://tools.ietf.org/html/draft-ietf-sasl-crammd5-to-historic-00.html]
13+
# recommends using +SCRAM-*+ or +PLAIN+ protected by TLS instead. Additionally,
14+
# RFC8314[https://tools.ietf.org/html/rfc8314] discourage the use of cleartext
15+
# and recommends TLS version 1.2 or greater be used for all traffic.
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+
24+
def initialize(user, password)
25+
@user = user
26+
@password = password
27+
end
28+
29+
def hmac_md5(text, key)
30+
if key.length > 64
31+
key = Digest::MD5.digest(key)
32+
end
33+
34+
k_ipad = key + "\0" * (64 - key.length)
35+
k_opad = key + "\0" * (64 - key.length)
36+
for i in 0..63
37+
k_ipad[i] = (k_ipad[i].ord ^ 0x36).chr
38+
k_opad[i] = (k_opad[i].ord ^ 0x5c).chr
39+
end
40+
41+
digest = Digest::MD5.digest(k_ipad + text)
42+
43+
return Digest::MD5.hexdigest(k_opad + digest)
44+
end
45+
46+
Net::IMAP.add_authenticator "PLAIN", self
47+
end

0 commit comments

Comments
 (0)