Skip to content

📚 Update SASL docs and add attr_readers #176

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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