Skip to content

Commit

Permalink
🔒 Add SASL ANONYMOUS mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
nevans committed Sep 15, 2023
1 parent 43a73ec commit 7649875
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 0 deletions.
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
124 changes: 124 additions & 0 deletions lib/net/imap/sasl/anonymous_authenticator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# 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:
# initial_response? -> true
#
# +ANONYMOUS+ can send an initial client response.
def initial_response?; true end

##
# :call-seq:
# new -> authenticator
# new(anonymous_message, **) -> authenticator
# new(anonymous_message:, **) -> authenticator
# new(message:, **) -> authenticator
# new {|propname, auth_ctx| propval } -> 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.
#
# ==== Configuration parameters
# Only one optional parameter::
#
# * #anonymous_message --- an optional message sent to the server which
# doesn't contain an <tt>"@"</tt> character, or if it does have an
# <tt>"@"</tt> it must be a valid email address.
#
# May be sent as positional argument or as a keyword argument.
# Aliased as #message.
#
# See Net::IMAP::SASL::Authenticator@Properties for a detailed
# description of attribute assignment, lazy loading, and callbacks.
def initialize(message_arg = nil, anonymous_message: nil, message: nil)
@anonymous_message = anonymous_message || message || message_arg
end

##
# method: anonymous_message
# :call-seq:
# anonymous_message -> string or nil
#
# A token sent for the +ANONYMOUS+ mechanism.
#
# Restricted to 255 UTF8 encoded characters, which will be validated by
# #process.
#
# If an "@" sign is included, the message must be a valid email address
# (+addr-spec+ from RFC-2822[https://tools.ietf.org/html/rfc2822]).
# Email syntax will _not_ be validated by AnonymousAuthenticator.
#
# Otherwise, it can be any UTF8 string which is permitted by the
# StringPrep "+trace+" profile. This is validated by #process.
# See AnonymousAuthenticator.stringprep_trace.
attr_reader :anonymous_message
alias message anonymous_message

# From RFC-4505[https://tools.ietf.org/html/rfc4505] §3, The "trace"
# Profile of "Stringprep":
# >>>
# Characters from the following tables of [StringPrep] are prohibited:
#
# - C.2.1 (ASCII control characters)
# - C.2.2 (Non-ASCII control characters)
# - C.3 (Private use characters)
# - C.4 (Non-character code points)
# - C.5 (Surrogate codes)
# - C.6 (Inappropriate for plain text)
# - C.8 (Change display properties are deprecated)
# - C.9 (Tagging characters)
#
# No additional characters are prohibited.
SASLPREP_TRACE_TABLES = %w[C.2.1 C.2.2 C.3 C.4 C.5 C.6 C.8 C.9].freeze

# From RFC-4505[https://tools.ietf.org/html/rfc4505] §3, The "trace"
# Profile of "Stringprep":
# >>>
# The character repertoire of this profile is Unicode 3.2 [Unicode].
#
# No mapping is required by this profile.
#
# No Unicode normalization is required by this profile.
#
# The list of unassigned code points for this profile is that provided
# in Appendix A of [StringPrep]. Unassigned code points are not
# prohibited.
#
# Characters from the following tables of [StringPrep] are prohibited:
# (documented on SASLPREP_TRACE_TABLES)
#
# This profile requires bidirectional character checking per Section 6
# of [StringPrep].
def self.stringprep_trace(string)
StringPrep.check_prohibited!(string,
*SASLPREP_TRACE_TABLES,
bidi: true,
profile: "trace")
string
end

# Returns the #anonymous_message, after checking it with
# rdoc-ref:AnonymousAuthenticator.stringprep_trace.
def process(_server_challenge_string)
if (size = anonymous_message&.length)&.> 255
raise Error, "anonymous_message is too long. (%d codepoints)" % [
size
]
end
self.class.stringprep_trace(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
37 changes: 37 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,43 @@ 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(Net::IMAP::Error) {
anonymous("a" * 256).process(nil)
}
end

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

0 comments on commit 7649875

Please sign in to comment.