Skip to content

Commit be02ca0

Browse files
committed
✨ SASL SCRAM-SHA-*: Add mechanisms [🚧 more tests, credit]
Also, don't forget to credit the PR on net-sasl for getting this started!
1 parent c1dd6be commit be02ca0

File tree

6 files changed

+522
-1
lines changed

6 files changed

+522
-1
lines changed

lib/net/imap.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -976,6 +976,15 @@ def starttls(options = {}, verify = true)
976976
# +PLAIN+:: See SASL::PlainAuthenticator.
977977
# Login using clear-text username and password.
978978
#
979+
# +SCRAM-*+:: See SASL::ScramAuthenticator.
980+
# Login by username and password. The password is not sent
981+
# to the server but is used in a salted challenge/response
982+
# exchange. One of the benefits over +PLAIN+ is that the
983+
# server cannot impersonate the user to other servers.
984+
# +SCRAM-SHA-1+ and +SCRAM-SHA-256+ are supported, but any
985+
# algorithm supported by OpenSSL::Digest can easily be
986+
# added.
987+
#
979988
# +OAUTHBEARER+:: See SASL::OAuthBearerAuthenticator.
980989
# Login using an OAUTH2 Bearer token. This is the
981990
# standard mechanism for using OAuth2 with \SASL, but it

lib/net/imap/authenticators.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ def authenticators
6767
require_relative "sasl/external_authenticator"
6868
require_relative "sasl/oauthbearer_authenticator"
6969
require_relative "sasl/plain_authenticator"
70+
require_relative "sasl/scram_authenticator"
7071
require_relative "sasl/xoauth2_authenticator"
7172

7273
# deprecated

lib/net/imap/sasl/authenticator.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ module SASL
2222
# +PLAIN+:: See SASL::PlainAuthenticator.
2323
# Login using clear-text username and password.
2424
#
25+
# +SCRAM-*+:: See SASL::ScramAuthenticator.
26+
# Login by username and password. The password is not sent
27+
# to the server but is used in a salted challenge/response
28+
# exchange. One of the benefits over +PLAIN+ is that the
29+
# server cannot impersonate the user to other servers.
30+
# +SCRAM-SHA-1+ and +SCRAM-SHA-256+ are supported, but any
31+
# algorithm supported by OpenSSL::Digest can easily be
32+
# added.
33+
#
2534
# +OAUTHBEARER+:: See SASL::OAuthBearerAuthenticator.
2635
# Login using an OAUTH2 Bearer token. This is the
2736
# standard mechanism for using OAuth2 with \SASL, but it
@@ -102,7 +111,6 @@ module SASL
102111
# * +scram_sha1_salted_passwords+, +scram_sha256_salted_password+ ---
103112
# Salted password(s) (with salt and iteration count) for the +SCRAM-*+
104113
# mechanism family. <tt>[salt, iterations, pbkdf2_hmac]</tt> tuple.
105-
# <em>(not implemented yet...)</em>
106114
# * +passcode+ --- passcode for SecurID 2FA <em>(not implemented)</em>
107115
# * +pin+ --- Personal Identification number, e.g. for SecurID 2FA
108116
# <em>(not implemented)</em>

lib/net/imap/sasl/scram_algorithm.rb

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

0 commit comments

Comments
 (0)