Skip to content

Commit

Permalink
🔀 Merge pull request #176 from ruby/fix-sasl-rdoc
Browse files Browse the repository at this point in the history
📚 Update SASL docs and add attr_readers
  • Loading branch information
nevans authored Sep 20, 2023
2 parents 9d8460c + 3fdc9f6 commit e64dbc8
Show file tree
Hide file tree
Showing 11 changed files with 255 additions and 129 deletions.
6 changes: 3 additions & 3 deletions lib/net/imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ module Net
# sending them. Special care should be taken to follow the #capabilities
# requirements for #starttls, #login, and #authenticate.
#
# See #capable?, #auth_capable, #capabilities, #auth_mechanisms to discover
# See #capable?, #auth_capable?, #capabilities, #auth_mechanisms to discover
# server capabilities. For relevant capability requirements, see the
# documentation on each \IMAP command.
#
Expand Down Expand Up @@ -1139,13 +1139,13 @@ def starttls(options = {}, verify = true)
# the documentation for the specific mechanisms you are using:
#
# +ANONYMOUS+::
# See AnonymousAuthenticator[Net::IMAP::SASL::AnonymousAuthenticator].
# See AnonymousAuthenticator[rdoc-ref:Net::IMAP::SASL::AnonymousAuthenticator].
#
# Allows the user to gain access to public services or resources without
# authenticating or disclosing an identity.
#
# +EXTERNAL+::
# See ExternalAuthenticator[Net::IMAP::SASL::ExternalAuthenticator].
# See ExternalAuthenticator[rdoc-ref:Net::IMAP::SASL::ExternalAuthenticator].
#
# Authenticates using already established credentials, such as a TLS
# certificate or IPsec.
Expand Down
4 changes: 2 additions & 2 deletions lib/net/imap/sasl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ class IMAP
# the documentation for the specific mechanisms you are using:
#
# +ANONYMOUS+::
# See AnonymousAuthenticator[Net::IMAP::SASL::AnonymousAuthenticator].
# See AnonymousAuthenticator.
#
# Allows the user to gain access to public services or resources without
# authenticating or disclosing an identity.
#
# +EXTERNAL+::
# See ExternalAuthenticator[Net::IMAP::SASL::ExternalAuthenticator].
# See ExternalAuthenticator.
#
# Authenticates using already established credentials, such as a TLS
# certificate or IPsec.
Expand Down
27 changes: 15 additions & 12 deletions lib/net/imap/sasl/anonymous_authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ module SASL
# Net::IMAP#authenticate.
class AnonymousAuthenticator

# An optional token sent for the +ANONYMOUS+ mechanism., up to 255 UTF-8
# characters in length.
#
# If it contains an "@" sign, the message must be a valid email address
# (+addr-spec+ from RFC-2822[https://tools.ietf.org/html/rfc2822]).
# Email syntax is _not_ validated by AnonymousAuthenticator.
#
# Otherwise, it can be any UTF8 string which is permitted by the
# StringPrep::Trace profile.
attr_reader :anonymous_message

# :call-seq:
# new(anonymous_message = "", **) -> authenticator
# new(anonymous_message: "", **) -> authenticator
Expand All @@ -21,7 +32,7 @@ class AnonymousAuthenticator
# #anonymous_message is an optional message which is sent to the server.
# It may be sent as a positional argument or as a keyword argument.
#
# Any other keyword parameters are quietly ignored.
# Any other keyword arguments are silently ignored.
def initialize(anon_msg = nil, anonymous_message: nil, **)
message = (anonymous_message || anon_msg || "").to_str
@anonymous_message = StringPrep::Trace.stringprep_trace message
Expand All @@ -31,24 +42,16 @@ def initialize(anon_msg = nil, anonymous_message: nil, **)
end
end

# A token sent for the +ANONYMOUS+ mechanism.
#
# If it contains an "@" sign, the message must be a valid email address
# (+addr-spec+ from RFC-2822[https://tools.ietf.org/html/rfc2822]).
# Email syntax is _not_ validated by AnonymousAuthenticator.
#
# Otherwise, it can be any UTF8 string which is permitted by the
# StringPrep::Trace profile, up to 255 UTF-8 characters in length.
attr_reader :anonymous_message

# :call-seq:
# initial_response? -> true
#
# +ANONYMOUS+ can send an initial client response.
def initial_response?; true end

# Returns #anonymous_message.
def process(_server_challenge_string) anonymous_message end
def process(_server_challenge_string)
anonymous_message
end

end
end
Expand Down
14 changes: 7 additions & 7 deletions lib/net/imap/sasl/cram_md5_authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,6 @@
# of cleartext and recommends TLS version 1.2 or greater be used for all
# traffic. With TLS +CRAM-MD5+ is okay, but so is +PLAIN+
class Net::IMAP::SASL::CramMD5Authenticator
def process(challenge)
digest = hmac_md5(challenge, @password)
return @user + " " + digest
end

private

def initialize(user, password, warn_deprecation: true, **_ignored)
if warn_deprecation
warn "WARNING: CRAM-MD5 mechanism is deprecated." # TODO: recommend SCRAM
Expand All @@ -30,6 +23,13 @@ def initialize(user, password, warn_deprecation: true, **_ignored)
@password = password
end

def process(challenge)
digest = hmac_md5(challenge, @password)
return @user + " " + digest
end

private

def hmac_md5(text, key)
if key.length > 64
key = Digest::MD5.digest(key)
Expand Down
81 changes: 62 additions & 19 deletions lib/net/imap/sasl/digest_md5_authenticator.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,72 @@
# frozen_string_literal: true

# Net::IMAP authenticator for the "`DIGEST-MD5`" SASL mechanism type, specified
# in RFC2831(https://tools.ietf.org/html/rfc2831). See Net::IMAP#authenticate.
# in RFC-2831[https://tools.ietf.org/html/rfc2831]. See Net::IMAP#authenticate.
#
# == Deprecated
#
# "+DIGEST-MD5+" has been deprecated by
# {RFC6331}[https://tools.ietf.org/html/rfc6331] and should not be relied on for
# RFC-6331[https://tools.ietf.org/html/rfc6331] and should not be relied on for
# security. It is included for compatibility with existing servers.
class Net::IMAP::SASL::DigestMD5Authenticator
STAGE_ONE = :stage_one
STAGE_TWO = :stage_two
private_constant :STAGE_ONE, :STAGE_TWO

# Authentication identity: the identity that matches the #password.
#
# RFC-2831[https://tools.ietf.org/html/rfc2831] uses the term +username+.
# "Authentication identity" is the generic term used by
# RFC-4422[https://tools.ietf.org/html/rfc4422].
# RFC-4616[https://tools.ietf.org/html/rfc4616] and many later RFCs abbreviate
# that to +authcid+. So +authcid+ is available as an alias for #username.
attr_reader :username

# A password or passphrase that matches the #username.
#
# The +password+ will be used to create the response digest.
attr_reader :password

# Authorization identity: an identity to act as or on behalf of. The identity
# form is application protocol specific. If not provided or left blank, the
# server derives an authorization identity from the authentication identity.
# The server is responsible for verifying the client's credentials and
# verifying that the identity it associates with the client's authentication
# identity is allowed to act as (or on behalf of) the authorization identity.
#
# For example, an administrator or superuser might take on another role:
#
# imap.authenticate "DIGEST-MD5", "root", ->{passwd}, authzid: "user"
#
attr_reader :authzid

# :call-seq:
# new(username, password, authzid = nil) -> authenticator
#
# Creates an Authenticator for the "+DIGEST-MD5+" SASL mechanism.
#
# Called by Net::IMAP#authenticate and similar methods on other clients.
#
# ==== Parameters
#
# * #username — Identity whose #password is used.
# * #password — A password or passphrase associated with this #username.
# * #authzid ― Alternate identity to act as or on behalf of. Optional.
# * +warn_deprecation+ — Set to +false+ to silence the warning.
#
# See the documentation for each attribute for more details.
def initialize(username, password, authzid = nil, warn_deprecation: true)
if warn_deprecation
warn "WARNING: DIGEST-MD5 SASL mechanism was deprecated by RFC6331."
# TODO: recommend SCRAM instead.
end
require "digest/md5"
require "strscan"
@username, @password, @authzid = username, password, authzid
@nc, @stage = {}, STAGE_ONE
end

# Responds to server challenge in two stages.
def process(challenge)
case @stage
when STAGE_ONE
Expand All @@ -31,7 +89,7 @@ def process(challenge)

response = {
:nonce => sparams['nonce'],
:username => @user,
:username => @username,
:realm => sparams['realm'],
:cnonce => Digest::MD5.hexdigest("%.15f:%.15f:%d" % [Time.now.to_f, rand, Process.pid.to_s]),
:'digest-uri' => 'imap/' + sparams['realm'],
Expand All @@ -41,7 +99,7 @@ def process(challenge)
:charset => sparams['charset'],
}

response[:authzid] = @authname unless @authname.nil?
response[:authzid] = @authzid unless @authzid.nil?

# now, the real thing
a0 = Digest::MD5.digest( [ response.values_at(:username, :realm), @password ].join(':') )
Expand Down Expand Up @@ -74,23 +132,8 @@ def process(challenge)
end
end

def initialize(user, password, authname = nil, warn_deprecation: true)
if warn_deprecation
warn "WARNING: DIGEST-MD5 SASL mechanism was deprecated by RFC6331."
# TODO: recommend SCRAM instead.
end
require "digest/md5"
require "strscan"
@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
Expand Down
12 changes: 6 additions & 6 deletions lib/net/imap/sasl/external_authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ module SASL
# established external to SASL, for example by TLS certificate or IPsec.
class ExternalAuthenticator

# Authorization identity: an identity to act as or on behalf of.
#
# If not explicitly provided, the server defaults to using the identity
# that was authenticated by the external credentials.
attr_reader :authzid

# :call-seq:
# new(authzid: nil, **) -> authenticator
#
Expand All @@ -30,12 +36,6 @@ def initialize(authzid: nil)
end
end

# Authorization identity: an identity to act as or on behalf of.
#
# If not explicitly provided, the server defaults to using the identity
# that was authenticated by the external credentials.
attr_reader :authzid

# :call-seq:
# initial_response? -> true
#
Expand Down
22 changes: 10 additions & 12 deletions lib/net/imap/sasl/login_authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,9 @@
# {draft-murchison-sasl-login}[https://www.iana.org/go/draft-murchison-sasl-login]
# for both specification and deprecation.
class Net::IMAP::SASL::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
private_constant :STATE_USER, :STATE_PASSWORD

def initialize(user, password, warn_deprecation: true, **_ignored)
if warn_deprecation
Expand All @@ -42,4 +31,13 @@ def initialize(user, password, warn_deprecation: true, **_ignored)
@state = STATE_USER
end

def process(data)
case @state
when STATE_USER
@state = STATE_PASSWORD
return @user
when STATE_PASSWORD
return @password
end
end
end
Loading

0 comments on commit e64dbc8

Please sign in to comment.