-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for SCRAM-SHA-* #5
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
# frozen_string_literal: true | ||
|
||
require "idn" | ||
require "openssl" | ||
require "securerandom" | ||
|
||
module Net | ||
|
||
module SASL | ||
|
||
# Authenticator for the "`SCRAM`" family of SASL mechanism types specified | ||
# in RFC5802(https://tools.ietf.org/html/rfc5802). | ||
class ScramAuthenticator < Authenticator | ||
|
||
def self.for(hash) | ||
Class.new(ScramAuthenticator) do | ||
define_method :initialize do |*args, **options| | ||
super(*args, hash: hash, **options) | ||
end | ||
end | ||
end | ||
|
||
# Provide the +username+ and +password+ credentials. An optional | ||
# +authzid+ is defined as: "The "authorization ID" as per | ||
# RFC2222[https://tools.ietf.org/html/rfc2222], | ||
# encoded in UTF-8. optional. If present, and the | ||
# authenticating user has sufficient privilege, and the server supports | ||
# it, then after authentication the server will use this identity for | ||
# making all accesses and access checks. If the client specifies it, and | ||
# the server does not support it, then the response-value will be | ||
# incorrect, and authentication will fail." | ||
# | ||
# This should generally be instantiated via Net::SASL.authenticator. | ||
def initialize(username, password, authzid = nil, hash:, **options) | ||
super | ||
@hash = OpenSSL::Digest.new(hash) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I'd prefer to define hash on the subclasses. It loses the nice one liner for new class SCRAMAuthenticator
def hash; @hash ||= OpenSSL::Digest.new(self.class::HASH) end
end
# Defined in RFC5802, etc etc etc
class SCRAM256Authenticator < SCRAMAuthenticator
HASH = "SHA256"
end The one-liner could be recreated with only a little bit of extra work. But I'm not sure it's worth it, especially because I prefer an explicitly named class constant to which we can add rdoc for each specific mechanism. |
||
@cnonce = options[:cnonce] || SecureRandom.hex(32) | ||
@done = false | ||
end | ||
|
||
def supports_initial_response? | ||
true | ||
end | ||
|
||
def done? | ||
@done | ||
end | ||
|
||
# responds to the server's challenges | ||
def process(challenge) | ||
return "n,#{'a=' + @authzid if @authzid},#{initial_message}" if challenge.nil? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can't rely on |
||
|
||
sparams = challenge.split(/,/).each_with_object({}) do |pair, h| | ||
k, v = pair.split(/=/) | ||
h[k] = v | ||
end | ||
|
||
if @server_signature | ||
@done = sparams["v"].unpack("m").first == @server_signature | ||
return if @done | ||
|
||
raise ChallengeParseError, "Bad server signature" | ||
end | ||
|
||
bare = "c=biws,r=#{sparams['r']}" | ||
salted_password = OpenSSL::KDF.pbkdf2_hmac( | ||
IDN::Stringprep.with_profile(@password.encode("utf-8"), "SASLprep"), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have a pure ruby stdlib implementation of SASLprep in mind for this but it isn't ready yet. nearly done, but not quite. The RFC does give us an official way to cheat: only allow ASCII strings. ;) So this could just guard |
||
salt: sparams["s"].unpack("m").first, | ||
iterations: sparams["i"].to_i, | ||
length: @hash.digest_length, | ||
hash: @hash | ||
) | ||
client_key = OpenSSL::HMAC.digest(@hash, salted_password, "Client Key") | ||
stored_key = @hash.digest(client_key) | ||
auth_message = "#{initial_message},#{challenge},#{bare}" | ||
client_signature = OpenSSL::HMAC.digest(@hash, stored_key, auth_message) | ||
client_proof = client_key.bytes.zip(client_signature.bytes).map { |x,y| (x ^ y).chr }.join | ||
server_key = OpenSSL::HMAC.digest(@hash, salted_password, "Server Key") | ||
@server_signature = OpenSSL::HMAC.digest(@hash, server_key, auth_message) | ||
"#{bare},p=#{[client_proof].pack('m').chomp}" | ||
end | ||
|
||
protected | ||
|
||
def initial_message | ||
"n=#{@username},r=#{@cnonce}" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should do some error checking or formatting on these values, here or in #initialize? eg "\0" or "," chars. |
||
end | ||
|
||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,5 +28,6 @@ Gem::Specification.new do |spec| | |
spec.require_paths = ["lib"] | ||
|
||
spec.add_dependency "digest" | ||
spec.add_dependency "idn-ruby" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer a pure-ruby saslprep, to avoid dependencies. I'm hoping to replace the SASL implementations in net/imap and net/pop and net/smtp, which are all bundled gems. Adding another dependency might prevent that from happening; especially a C dependency, which isn't ideal for for JRuby and TruffleRuby. My plan is to allow a swap-able saslprep library, for performance. But that will need benchmarks. |
||
spec.add_dependency "strscan" | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like that it only takes one line of code. But, even though plugging in alternate hashes is fairly straightforward and predictable, I'd prefer to only include official SASL mechanisms in the default set. I think that the IETF drafts for 512 (or at least, the ones that I knew about) recently expired, although I haven't researched why.
For any that are left out, we can include documentation so that users can add it themselves, if they really need it sooner.