Skip to content

Commit

Permalink
✨ SASL SCRAM-SHA-*: Add mechanisms [🚧 more tests, credit]
Browse files Browse the repository at this point in the history
Also, don't forget to credit the PR on net-sasl for getting this
started!
  • Loading branch information
nevans committed Dec 20, 2022
1 parent 8d01d36 commit 8818e79
Show file tree
Hide file tree
Showing 6 changed files with 522 additions and 1 deletion.
9 changes: 9 additions & 0 deletions lib/net/imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,15 @@ def starttls(options = {}, verify = true)
# +PLAIN+:: See SASL::PlainAuthenticator.
# Login using clear-text username and password.
#
# +SCRAM-*+:: See SASL::ScramAuthenticator.
# Login by username and password. The password is not sent
# to the server but is used in a salted challenge/response
# exchange. One of the benefits over +PLAIN+ is that the
# server cannot impersonate the user to other servers.
# +SCRAM-SHA-1+ and +SCRAM-SHA-256+ are supported, but any
# algorithm supported by OpenSSL::Digest can easily be
# added.
#
# +OAUTHBEARER+:: See SASL::OAuthBearerAuthenticator.
# Login using an OAUTH2 Bearer token. This is the
# standard mechanism for using OAuth2 with \SASL, but it
Expand Down
1 change: 1 addition & 0 deletions lib/net/imap/authenticators.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def authenticators
require_relative "sasl/external_authenticator"
require_relative "sasl/oauthbearer_authenticator"
require_relative "sasl/plain_authenticator"
require_relative "sasl/scram_authenticator"
require_relative "sasl/xoauth2_authenticator"

# deprecated
Expand Down
10 changes: 9 additions & 1 deletion lib/net/imap/sasl/authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ module SASL
# +PLAIN+:: See SASL::PlainAuthenticator.
# Login using clear-text username and password.
#
# +SCRAM-*+:: See SASL::ScramAuthenticator.
# Login by username and password. The password is not sent
# to the server but is used in a salted challenge/response
# exchange. One of the benefits over +PLAIN+ is that the
# server cannot impersonate the user to other servers.
# +SCRAM-SHA-1+ and +SCRAM-SHA-256+ are supported, but any
# algorithm supported by OpenSSL::Digest can easily be
# added.
#
# +OAUTHBEARER+:: See SASL::OAuthBearerAuthenticator.
# Login using an OAUTH2 Bearer token. This is the
# standard mechanism for using OAuth2 with \SASL, but it
Expand Down Expand Up @@ -102,7 +111,6 @@ module SASL
# * +scram_sha1_salted_passwords+, +scram_sha256_salted_password+ ---
# Salted password(s) (with salt and iteration count) for the +SCRAM-*+
# mechanism family. <tt>[salt, iterations, pbkdf2_hmac]</tt> tuple.
# <em>(not implemented yet...)</em>
# * +passcode+ --- passcode for SecurID 2FA <em>(not implemented)</em>
# * +pin+ --- Personal Identification number, e.g. for SecurID 2FA
# <em>(not implemented)</em>
Expand Down
69 changes: 69 additions & 0 deletions lib/net/imap/sasl/scram_algorithm.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# frozen_string_literal: true

module Net
class IMAP
module SASL

# For method descriptions, see {RFC5802
# §2}[https://www.rfc-editor.org/rfc/rfc5802#section-2] and {RFC5802
# §3}[https://www.rfc-editor.org/rfc/rfc5802#section-3].
#
# Expects:
# * #Hi, #H, and #HMAC use:
# * +#digest+ --- an OpenSSL::Digest.
# * #salted_password uses:
# * +#salt+ and +#iterations+ --- the server's values for this user
# * +#password+
# * #auth_message is built from:
# * +#client_first_message_bare+ --- contains +#cnonce+
# * +#server_first_message+ --- contains +#snonce+
# * +#client_final_message_no_proof+ --- contains +#snonce+
module ScramAlgorithm
def Normalize(str) SASL.saslprep(str) end

def Hi(str, salt, iterations)
length = digest.digest_length
OpenSSL::KDF.pbkdf2_hmac(
str,
salt: salt,
iterations: iterations,
length: length,
hash: digest,
)
end

def H(str) digest.digest str end

def HMAC(key, data) OpenSSL::HMAC.digest(digest, key, data) end

def XOR(str1, str2)
str1.unpack("C*")
.zip(str2.unpack("C*"))
.map {|a, b| a ^ b }
.pack("C*")
end

def auth_message
[
client_first_message_bare,
server_first_message,
client_final_message_no_proof,
]
.join(",")
end

def salted_password
Hi(Normalize(password), salt, iterations)
end

def client_key; HMAC(salted_password, "Client Key") end
def server_key; HMAC(salted_password, "Server Key") end
def stored_key; H(client_key) end
def client_signature; HMAC(stored_key, auth_message) end
def server_signature; HMAC(server_key, auth_message) end
def client_proof; XOR(client_key, client_signature) end
end

end
end
end
Loading

0 comments on commit 8818e79

Please sign in to comment.