Skip to content

Commit

Permalink
✨ Add very basic SASL-IR support to #authenticate [🚧 WIP]
Browse files Browse the repository at this point in the history
Missing a couple of tests, but it should be working now.
And I'd like the default `:sasl_ir` value to be configurable.
  • Loading branch information
nevans committed Jul 27, 2023
1 parent b8f2986 commit 37fad2c
Show file tree
Hide file tree
Showing 8 changed files with 80 additions and 10 deletions.
25 changes: 18 additions & 7 deletions lib/net/imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1053,14 +1053,25 @@ def starttls(options = {}, verify = true)
# If the TaggedResponse to #authenticate includes updated capabilities, they
# will be cached.
#
def authenticate(mechanism, ...)
authenticator = self.class.authenticator(mechanism, ...)
send_command("AUTHENTICATE", mechanism) do |resp|
def authenticate(mechanism, *creds, sasl_ir: false, **props, &callback)
[nil, true, false, :auto].include?(sasl_ir) or
raise ArgumentError, "sasl_ir must be boolean or :auto"
authenticator = self.class.authenticator(mechanism,
*creds,
**props,
&callback)
cmdargs = ["AUTHENTICATE", mechanism]
sasl_ir = capable?("SASL-IR") if [nil, :auto].include?(sasl_ir)
if sasl_ir && SASL.initial_response?(authenticator)
response = authenticator.process(nil)
cmdargs << [response].pack("m0")
end
send_command(*cmdargs) do |resp|
if resp.instance_of?(ContinuationRequest)
data = authenticator.process(resp.data.text.unpack("m")[0])
s = [data].pack("m0")
send_string_data(s)
put_string(CRLF)
challenge = resp.data.text.unpack1("m")
response = authenticator.process(challenge)
response = [response].pack("m0")
put_string(response + CRLF)
end
end
.tap { @capabilities = capabilities_from_resp_code _1 }
Expand Down
2 changes: 2 additions & 0 deletions lib/net/imap/authenticators/plain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
# can be secured by TLS encryption.
class Net::IMAP::PlainAuthenticator

def initial_response?; true end

def process(data)
return "#@authzid\0#@username\0#@password"
end
Expand Down
4 changes: 4 additions & 0 deletions lib/net/imap/sasl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ def saslprep(string, **opts)
Net::IMAP::StringPrep::SASLprep.saslprep(string, **opts)
end

def initial_response?(mechanism)
mechanism.respond_to?(:initial_response?) && mechanism.initial_response?
end

end
end

Expand Down
2 changes: 1 addition & 1 deletion test/net/imap/fake_server/command_reader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def get_command
def parse(buf)
/\A([^ ]+) ((?:UID )?\w+)(?: (.+))?\r\n\z/min =~ buf or raise "bad request"
case $2.upcase
when "LOGIN", "SELECT", "ENABLE"
when "LOGIN", "SELECT", "ENABLE", "AUTHENTICATE"
Command.new $1, $2, scan_astrings($3), buf
else
Command.new $1, $2, $3, buf # TODO...
Expand Down
10 changes: 8 additions & 2 deletions test/net/imap/fake_server/command_router.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,14 @@ def handler_for(command)
on "AUTHENTICATE" do |resp|
state.not_authenticated? or return resp.fail_bad_state(state)
args = resp.command.args
args == "PLAIN" or return resp.fail_no "unsupported"
response_b64 = resp.request_continuation("") || ""
(1..2) === args.length or return resp.fail_bad_args
args.first == "PLAIN" or return resp.fail_no "unsupported"
if args.length == 2
response_b64 = args.last
else
response_b64 = resp.request_continuation("") || ""
state.commands << {continuation: response_b64}
end
response = Base64.decode64(response_b64)
response.empty? and return resp.fail_bad "canceled"
# TODO: support mechanisms other than PLAIN.
Expand Down
4 changes: 4 additions & 0 deletions test/net/imap/fake_server/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class Configuration
encrypted_login: true,
cleartext_auth: false,
sasl_mechanisms: %i[PLAIN].freeze,
sasl_ir: false,

rev1: true,
rev2: false,
Expand Down Expand Up @@ -66,6 +67,7 @@ def initialize(with_extensions: [], without_extensions: [], **opts, &block)
alias cleartext_auth? cleartext_auth
alias greeting_bye? greeting_bye
alias greeting_capabilities? greeting_capabilities
alias sasl_ir? sasl_ir

def on(event, &handler)
handler or raise ArgumentError
Expand Down Expand Up @@ -104,13 +106,15 @@ def capabilities_pre_tls
capa << "STARTTLS" if starttls?
capa << "LOGINDISABLED" unless cleartext_login?
capa.concat auth_capabilities if cleartext_auth?
capa << "SASL-IR" if sasl_ir? && cleartext_auth?
capa
end

def capabilities_pre_auth
capa = basic_capabilities
capa << "LOGINDISABLED" unless encrypted_login?
capa.concat auth_capabilities
capa << "SASL-IR" if sasl_ir?
capa
end

Expand Down
41 changes: 41 additions & 0 deletions test/net/imap/test_imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,47 @@ def test_id
end
end

test "#authenticate PLAIN (sasl_ir: default)" do
with_fake_server(preauth: false, cleartext_auth: true) do |server, imap|
imap.authenticate("PLAIN", "test_user", "test-password")
cmd, continuation = server.commands.pop, server.commands.pop
assert_empty server.commands
assert_equal "AUTHENTICATE", cmd.name
assert_equal ["PLAIN"], cmd.args
encoded = continuation[:continuation]
assert_equal "\x00test_user\x00test-password", Base64.decode64(encoded)
end
end

test "#authenticate(..., sasl_ir: :auto), SASL-IR not in capabilities" do
with_fake_server(preauth: false, cleartext_auth: true) do |server, imap|
refute imap.capable? "SASL-IR"
imap.authenticate("PLAIN", "test_user", "test-password", sasl_ir: :auto)
cmd, continuation = server.commands.pop, server.commands.pop
assert_empty server.commands
assert_equal "AUTHENTICATE", cmd.name
assert_equal ["PLAIN"], cmd.args
encoded = continuation[:continuation]
assert_equal "\x00test_user\x00test-password", Base64.decode64(encoded)
end
end

test "#authenticate(..., sasl_ir: :auto), SASL-IR in capabilities" do
with_fake_server(
preauth: false, cleartext_auth: true, sasl_ir: true
) do |server, imap|
assert imap.capable? "SASL-IR"
imap.authenticate("PLAIN", "test_user", "test-password", sasl_ir: :auto)
cmd = server.commands.pop
assert_empty server.commands
assert_equal "AUTHENTICATE", cmd.name
assert_equal(
["PLAIN", ["\x00test_user\x00test-password"].pack("m0")],
cmd.args
)
end
end

def test_uidplus_uid_expunge
with_fake_server(select: "INBOX",
extensions: %i[UIDPLUS]) do |server, imap|
Expand Down
2 changes: 2 additions & 0 deletions test/net/imap/test_imap_capabilities.rb
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ def teardown

imap.authenticate("PLAIN", "test_user", "test-password")
assert_equal "AUTHENTICATE", server.commands.pop.name
assert server.commands.pop[:continuation]
refute imap.capabilities_cached?

assert imap.capable? :IMAP4rev1
Expand Down Expand Up @@ -277,6 +278,7 @@ def teardown
rescue Net::IMAP::NoResponseError
end
assert_equal "AUTHENTICATE", server.commands.pop.name
assert server.commands.pop[:continuation]
assert_equal original_capabilities, imap.capabilities
assert_empty server.commands
end
Expand Down

0 comments on commit 37fad2c

Please sign in to comment.