Skip to content

Commit

Permalink
✨ SASL ANONYMOUS: Add new mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
nevans committed Jul 25, 2023
1 parent b6fdf18 commit afae8d0
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 0 deletions.
7 changes: 7 additions & 0 deletions lib/net/imap/sasl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ class IMAP
# Non-standard and obsoleted by +OAUTHBEARER+, but widely
# supported.
#
# +ANONYMOUS+:: See AnonymousAuthenticator.
# Allow the user to gain access to public services or
# resources without authenticating or disclosing an
# identity.
#
# === Deprecated mechanisms
#
# <em>Obsolete mechanisms are available for backwards compatibility. Using
Expand All @@ -62,6 +67,7 @@ module SASL
sasl_dir = File.expand_path("sasl", __dir__)
autoload :Authenticator, "#{sasl_dir}/authenticator"
autoload :Authenticators, "#{sasl_dir}/authenticators"
autoload :AnonymousAuthenticator, "#{sasl_dir}/anonymous_authenticator"
autoload :PlainAuthenticator, "#{sasl_dir}/plain_authenticator"
autoload :XOAuth2Authenticator, "#{sasl_dir}/xoauth2_authenticator"

Expand All @@ -72,6 +78,7 @@ module SASL
# Authenticators are all lazy loaded
def self.authenticators
@authenticators ||= SASL::Authenticators.new.tap do |registry|
registry.add_authenticator "Anonymous"
registry.add_authenticator "Plain"
registry.add_authenticator "XOAuth2"
registry.add_authenticator "Login" # deprecated
Expand Down
127 changes: 127 additions & 0 deletions lib/net/imap/sasl/anonymous_authenticator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# frozen_string_literal: true

require_relative "authenticator"

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 < Authenticator

# :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.
#
# ==== Properties
# Only one optional property:
#
# * #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 property assignment, lazy loading, and callbacks.
def initialize(message_arg = nil, anonymous_message: nil, message: nil)
super
propinit :anonymous_message, message_arg, anonymous_message, message
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.
property :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
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 @@ -86,6 +86,43 @@ def test_xoauth2_callbacks
)
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 afae8d0

Please sign in to comment.