From afae8d05f1aa4382f3f8f7e8037fd98d30923363 Mon Sep 17 00:00:00 2001 From: nick evans Date: Tue, 20 Dec 2022 09:46:38 -0500 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20SASL=20ANONYMOUS:=20Add=20new=20mec?= =?UTF-8?q?hanism?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/net/imap/sasl.rb | 7 + lib/net/imap/sasl/anonymous_authenticator.rb | 127 +++++++++++++++++++ test/net/imap/test_imap_authenticators.rb | 37 ++++++ 3 files changed, 171 insertions(+) create mode 100644 lib/net/imap/sasl/anonymous_authenticator.rb diff --git a/lib/net/imap/sasl.rb b/lib/net/imap/sasl.rb index 3e09b7f9..c13de790 100644 --- a/lib/net/imap/sasl.rb +++ b/lib/net/imap/sasl.rb @@ -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 # # Obsolete mechanisms are available for backwards compatibility. Using @@ -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" @@ -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 diff --git a/lib/net/imap/sasl/anonymous_authenticator.rb b/lib/net/imap/sasl/anonymous_authenticator.rb new file mode 100644 index 00000000..4b3a5a28 --- /dev/null +++ b/lib/net/imap/sasl/anonymous_authenticator.rb @@ -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 "@" character, or if it does have an + # "@" 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 diff --git a/test/net/imap/test_imap_authenticators.rb b/test/net/imap/test_imap_authenticators.rb index 5e143706..64c92ebc 100644 --- a/test/net/imap/test_imap_authenticators.rb +++ b/test/net/imap/test_imap_authenticators.rb @@ -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) # ----------------------