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

lib/net/imap.rb

Lines changed: 2 additions & 204 deletions
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"

lib/net/imap/authenticators.rb

Lines changed: 44 additions & 0 deletions
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"
Lines changed: 47 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy