Skip to content

Commit 8764a3a

Browse files
nevanssingpolyma
andcommitted
🔒 Add SASL SCRAM-SHA-* mechanisms
Based on the implementation by @singpolyma at nevans/net-sasl#5 Co-authored-by: Stephen Paul Weber <[email protected]>
1 parent ba5f5a1 commit 8764a3a

File tree

9 files changed

+480
-11
lines changed

9 files changed

+480
-11
lines changed

lib/net/imap.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,6 +1026,17 @@ def starttls(options = {}, verify = true)
10261026
#
10271027
# Login using clear-text username and password.
10281028
#
1029+
# +SCRAM-SHA-1+::
1030+
# +SCRAM-SHA-256+::
1031+
# See ScramAuthenticator[rdoc-ref:Net::IMAP::SASL::ScramAuthenticator].
1032+
#
1033+
# Login by username and password. The password is not sent to the
1034+
# server but is used in a salted challenge/response exchange.
1035+
# +SCRAM-SHA-1+ and +SCRAM-SHA-256+ are directly supported by
1036+
# Net::IMAP::SASL. New authenticators can easily be added for any other
1037+
# <tt>SCRAM-*</tt> mechanism if the digest algorithm is supported by
1038+
# OpenSSL::Digest.
1039+
#
10291040
# +XOAUTH2+::
10301041
# See XOAuth2Authenticator[rdoc-ref:Net::IMAP::SASL::XOAuth2Authenticator].
10311042
#

lib/net/imap/sasl.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ class IMAP
5151
#
5252
# Login using clear-text username and password.
5353
#
54+
# +SCRAM-SHA-1+::
55+
# +SCRAM-SHA-256+::
56+
# See ScramAuthenticator.
57+
#
58+
# Login by username and password. The password is not sent to the
59+
# server but is used in a salted challenge/response exchange.
60+
# +SCRAM-SHA-1+ and +SCRAM-SHA-256+ are directly supported by
61+
# Net::IMAP::SASL. New authenticators can easily be added for any other
62+
# <tt>SCRAM-*</tt> mechanism if the digest algorithm is supported by
63+
# OpenSSL::Digest.
64+
#
5465
# +XOAUTH2+::
5566
# See XOAuth2Authenticator.
5667
#
@@ -89,10 +100,15 @@ module SASL
89100
sasl_dir = File.expand_path("sasl", __dir__)
90101
autoload :Authenticators, "#{sasl_dir}/authenticators"
91102
autoload :GS2Header, "#{sasl_dir}/gs2_header"
103+
autoload :ScramAlgorithm, "#{sasl_dir}/scram_algorithm"
104+
autoload :ScramAuthenticator, "#{sasl_dir}/scram_authenticator"
105+
92106
autoload :AnonymousAuthenticator, "#{sasl_dir}/anonymous_authenticator"
93107
autoload :ExternalAuthenticator, "#{sasl_dir}/external_authenticator"
94108
autoload :OAuthBearerAuthenticator, "#{sasl_dir}/oauthbearer_authenticator"
95109
autoload :PlainAuthenticator, "#{sasl_dir}/plain_authenticator"
110+
autoload :ScramSHA1Authenticator, "#{sasl_dir}/scram_sha1_authenticator"
111+
autoload :ScramSHA256Authenticator, "#{sasl_dir}/scram_sha256_authenticator"
96112
autoload :XOAuth2Authenticator, "#{sasl_dir}/xoauth2_authenticator"
97113

98114
autoload :CramMD5Authenticator, "#{sasl_dir}/cram_md5_authenticator"

lib/net/imap/sasl/authenticators.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ def initialize(use_defaults: false)
3737
add_authenticator "External"
3838
add_authenticator "OAuthBearer"
3939
add_authenticator "Plain"
40+
add_authenticator "Scram-SHA-1"
41+
add_authenticator "Scram-SHA-256"
4042
add_authenticator "XOAuth2"
4143
add_authenticator "Login" # deprecated
4244
add_authenticator "Cram-MD5" # deprecated

lib/net/imap/sasl/gs2_header.rb

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@ module Net
44
class IMAP < Protocol
55
module SASL
66

7-
# Several mechanisms start with a GS2 header:
8-
# * +GS2-*+
9-
# * +SCRAM-*+ --- ScramAuthenticator
10-
# * +OPENID20+
11-
# * +SAML20+
12-
# * +OAUTH10A+
13-
# * +OAUTHBEARER+ --- OAuthBearerAuthenticator
7+
# Originally defined for the GS2 mechanism family in
8+
# RFC5801[https://tools.ietf.org/html/rfc5801],
9+
# several different mechanisms start with a GS2 header:
10+
# * +GS2-*+ --- RFC5801[https://tools.ietf.org/html/rfc5801]
11+
# * +SCRAM-*+ --- RFC5802[https://tools.ietf.org/html/rfc5802],
12+
# see ScramAuthenticator.
13+
# * +SAML20+ --- RFC6595[https://tools.ietf.org/html/rfc6595]
14+
# * +OPENID20+ --- RFC6616[https://tools.ietf.org/html/rfc6616]
15+
# * +OAUTH10A+ --- RFC7628[https://tools.ietf.org/html/rfc7628]
16+
# * +OAUTHBEARER+ --- RFC7628[https://tools.ietf.org/html/rfc7628],
17+
# see OAuthBearerAuthenticator..
1418
#
15-
# Classes that include this must implement +#authzid+.
19+
# Classes that include this module must implement +#authzid+.
1620
module GS2Header
1721
NO_NULL_CHARS = /\A[^\x00]+\z/u.freeze # :nodoc:
1822

@@ -60,9 +64,6 @@ def gs2_authzid
6064
module_function
6165

6266
# Encodes +str+ to match RFC5801_SASLNAME.
63-
#
64-
#--
65-
# TODO: validate NO_NULL_CHARS and valid UTF-8 in the attr_writer.
6667
def gs2_saslname_encode(str)
6768
str = str.encode("UTF-8")
6869
# Regexp#match raises "invalid byte sequence" for invalid UTF-8

lib/net/imap/sasl/scram_algorithm.rb

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# frozen_string_literal: true
2+
3+
module Net
4+
class IMAP
5+
module SASL
6+
7+
# For method descriptions,
8+
# see {RFC5802 §2.2}[https://www.rfc-editor.org/rfc/rfc5802#section-2.2]
9+
# and {RFC5802 §3}[https://www.rfc-editor.org/rfc/rfc5802#section-3].
10+
module ScramAlgorithm
11+
def Normalize(str) SASL.saslprep(str) end
12+
13+
def Hi(str, salt, iterations)
14+
length = digest.digest_length
15+
OpenSSL::KDF.pbkdf2_hmac(
16+
str,
17+
salt: salt,
18+
iterations: iterations,
19+
length: length,
20+
hash: digest,
21+
)
22+
end
23+
24+
def H(str) digest.digest str end
25+
26+
def HMAC(key, data) OpenSSL::HMAC.digest(digest, key, data) end
27+
28+
def XOR(str1, str2)
29+
str1.unpack("C*")
30+
.zip(str2.unpack("C*"))
31+
.map {|a, b| a ^ b }
32+
.pack("C*")
33+
end
34+
35+
def auth_message
36+
[
37+
client_first_message_bare,
38+
server_first_message,
39+
client_final_message_without_proof,
40+
]
41+
.join(",")
42+
end
43+
44+
def salted_password
45+
Hi(Normalize(password), salt, iterations)
46+
end
47+
48+
def client_key; HMAC(salted_password, "Client Key") end
49+
def server_key; HMAC(salted_password, "Server Key") end
50+
def stored_key; H(client_key) end
51+
def client_signature; HMAC(stored_key, auth_message) end
52+
def server_signature; HMAC(server_key, auth_message) end
53+
def client_proof; XOR(client_key, client_signature) end
54+
end
55+
56+
end
57+
end
58+
end

0 commit comments

Comments
 (0)