Skip to content
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

🔒 Add SASL ANONYMOUS mechanism #169

Merged
merged 1 commit into from
Sep 16, 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: 6 additions & 0 deletions lib/net/imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,12 @@ def starttls(options = {}, verify = true)
# Each mechanism has different properties and requirements. Please consult
# the documentation for the specific mechanisms you are using:
#
# +ANONYMOUS+::
# See AnonymousAuthenticator[Net::IMAP::SASL::AnonymousAuthenticator].
#
# Allows the user to gain access to public services or resources without
# authenticating or disclosing an identity.
#
# +OAUTHBEARER+::
# See OAuthBearerAuthenticator[rdoc-ref:Net::IMAP::SASL::OAuthBearerAuthenticator].
#
Expand Down
8 changes: 8 additions & 0 deletions lib/net/imap/sasl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ class IMAP
# Each mechanism has different properties and requirements. Please consult
# the documentation for the specific mechanisms you are using:
#
# +ANONYMOUS+::
# See AnonymousAuthenticator[Net::IMAP::SASL::AnonymousAuthenticator].
#
# Allows the user to gain access to public services or resources without
# authenticating or disclosing an identity.
#
# +OAUTHBEARER+::
# See OAuthBearerAuthenticator.
#
Expand Down Expand Up @@ -77,6 +83,8 @@ module SASL
sasl_dir = File.expand_path("sasl", __dir__)
autoload :Authenticators, "#{sasl_dir}/authenticators"
autoload :GS2Header, "#{sasl_dir}/gs2_header"

autoload :AnonymousAuthenticator, "#{sasl_dir}/anonymous_authenticator"
autoload :OAuthBearerAuthenticator, "#{sasl_dir}/oauthbearer_authenticator"
autoload :PlainAuthenticator, "#{sasl_dir}/plain_authenticator"
autoload :XOAuth2Authenticator, "#{sasl_dir}/xoauth2_authenticator"
Expand Down
56 changes: 56 additions & 0 deletions lib/net/imap/sasl/anonymous_authenticator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

module Net
class IMAP < Protocol
module SASL

# Authenticator for the "+ANONYMOUS+" SASL mechanism, as specified by
# RFC-4505[https://tools.ietf.org/html/rfc4505]. See
# Net::IMAP#authenticate.
class AnonymousAuthenticator

# :call-seq:
# new(anonymous_message = "", **) -> authenticator
# new(anonymous_message: "", **) -> authenticator
#
# Creates an Authenticator for the "+ANONYMOUS+" SASL mechanism, as
# specified in RFC-4505[https://tools.ietf.org/html/rfc4505]. To use
# this, see Net::IMAP#authenticate or your client's authentication
# method.
#
# #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.
def initialize(anon_msg = nil, anonymous_message: nil, **)
message = (anonymous_message || anon_msg || "").to_str
@anonymous_message = StringPrep::Trace.stringprep_trace message
if (size = @anonymous_message&.length)&.> 255
raise ArgumentError,
"anonymous_message is too long. (%d codepoints)" % [size]
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

end
end
end
end
1 change: 1 addition & 0 deletions lib/net/imap/sasl/authenticators.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class Authenticators
def initialize(use_defaults: false)
@authenticators = {}
if use_defaults
add_authenticator "Anonymous"
add_authenticator "OAuthBearer"
add_authenticator "Plain"
add_authenticator "XOAuth2"
Expand Down
35 changes: 35 additions & 0 deletions test/net/imap/test_imap_authenticators.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,41 @@ def test_xoauth2_supports_initial_response
assert Net::IMAP::SASL.initial_response?(xoauth2("foo", "bar"))
end

# ----------------------
# ANONYMOUS
# ----------------------

def anonymous(*args, **kwargs, &block)
Net::IMAP::SASL.authenticator("ANONYMOUS", *args, **kwargs, &block)
end

def test_anonymous_matches_mechanism
assert_kind_of(Net::IMAP::SASL::AnonymousAuthenticator, anonymous)
end

def test_anonymous_response
assert_equal("", anonymous.process(nil))
assert_equal("hello world", anonymous("hello world").process(nil))
assert_equal("kwargs",
anonymous(anonymous_message: "kwargs").process(nil))
end

def test_anonymous_stringprep
assert_raise(Net::IMAP::SASL::ProhibitedCodepoint) {
anonymous("no\ncontrol\rchars").process(nil)
}
assert_raise(Net::IMAP::SASL::ProhibitedCodepoint) {
anonymous("regional flags use tagging chars: e.g." \
"🏴󠁧󠁢󠁥󠁮󠁧󠁿 England, " \
"🏴󠁧󠁢󠁳󠁣󠁴󠁿 Scotland, " \
"🏴󠁧󠁢󠁷󠁬󠁳󠁿 Wales.").process(nil)
}
end

def test_anonymous_length_over_255
assert_raise(ArgumentError) { anonymous("a" * 256).process(nil) }
end

# ----------------------
# LOGIN (obsolete)
# ----------------------
Expand Down