class Net::IMAP::SASL::ScramAuthenticator
is not supported yet.</em>
Caching of salted_password, client_key, stored_key, and server_key<br><br>=== Caching SCRAM secrets<br><br>supported yet.
<em>The SCRAM-*-PLUS
mechanisms and channel binding are not
=== TLS Channel binding
server_error may contain error details.
If #process raises an Error for the server-final-message, then
authentication and can return server error data in the server messages.
Unlike many other SASL mechanisms, the SCRAM-*
family supports mutual
#server_error, etc.
the various attributes, e.g: #snonce, #salt, #iterations, #verifier,
As server messages are received, they are validated and loaded into
==== Server messages
See also the methods on GS2Header.-PLUS
).
which hash function that is used (or by support for channel binding with
overview of the algorithm. The different mechanisms differ only by
See the documentation and method definitions on ScramAlgorithm for an
=== SCRAM algorithm
Subclasses need only set an appropriate DIGEST_NAME
constant.<br>OpenSSL::Digest.
supported by
New SCRAM-*
mechanisms can easily be added for any hash algorithm
* SCRAM-SHA-256
— ScramSHA256Authenticator
* SCRAM-SHA-1
— ScramSHA1Authenticator
Directly supported:
Net::IMAP#authenticate.
defined in RFC5802. Use via
Abstract base class for the “SCRAM-*
” family of SASL mechanisms,
def client_final_message_without_proof
See {RFC5802 §7}[https://www.rfc-editor.org/rfc/rfc5802#section-7]
def client_final_message_without_proof @client_final_message_without_proof ||= format_message(c: [cbind_input].pack("m0"), # channel-binding r: snonce) # nonce end
def client_first_message_bare
See {RFC5802 §7}[https://www.rfc-editor.org/rfc/rfc5802#section-7]
def client_first_message_bare @client_first_message_bare ||= format_message(n: gs2_saslname_encode(SASL.saslprep(username)), r: cnonce) end
def digest; OpenSSL::Digest.new self.class::DIGEST_NAME end
The class's +DIGEST_NAME+ constant must be set to the name of an
function for the chosen mechanism.
Returns a new OpenSSL::Digest object, set to the appropriate hash
def digest; OpenSSL::Digest.new self.class::DIGEST_NAME end
def done?; @state == :done end
Is the authentication exchange complete?
def done?; @state == :done end
def final_message_with_proof
See {RFC5802 §7}[https://www.rfc-editor.org/rfc/rfc5802#section-7]
def final_message_with_proof proof = [client_proof].pack("m0") "#{client_final_message_without_proof},p=#{proof}" end
def format_message(hash) hash.map { _1.join("=") }.join(",") end
def format_message(hash) hash.map { _1.join("=") }.join(",") end
def initial_client_response
See {RFC5802 §7}[https://www.rfc-editor.org/rfc/rfc5802#section-7]
def initial_client_response "#{gs2_header}#{client_first_message_bare}" end
def initialize(username_arg = nil, password_arg = nil,
* _optional_ #min_iterations - Overrides the default value (4096).
* _optional_ #authzid ― Alternate identity to act as or on behalf of.
* #password ― Password or passphrase associated with this #username.
#username - An alias for #authcid.
* #authcid ― Identity whose #password is used.
=== Parameters
Called by Net::IMAP#authenticate and similar methods on other clients.
Each subclass defines #digest to match a specific mechanism.
Creates an authenticator for one of the "+SCRAM-*+" SASL mechanisms.
new(authcid:, password:, **options) -> auth_ctx
new(username:, password:, **options) -> auth_ctx
new(username, password, **options) -> auth_ctx
:call-seq:
def initialize(username_arg = nil, password_arg = nil, authcid: nil, username: nil, authzid: nil, password: nil, secret: nil, min_iterations: 4096, # see both RFC5802 and RFC7677 cnonce: nil, # must only be set in tests **options) @username = username || username_arg || authcid or raise ArgumentError, "missing username (authcid)" @password = password || secret || password_arg or raise ArgumentError, "missing password" @authzid = authzid @min_iterations = Integer min_iterations @min_iterations.positive? or raise ArgumentError, "min_iterations must be positive" @cnonce = cnonce || SecureRandom.base64(32) end
def parse_challenge(challenge)
this parses it simply as a hash, without respect to order. Note that
messages is fixed, with the exception of extension attributes", but
RFC5802 specifies "that the order of attributes in client or server
def parse_challenge(challenge) challenge.split(/,/).to_h {|pair| pair.split(/=/, 2) } rescue ArgumentError raise Error, "unparsable challenge: %p" % [challenge] end
def process(challenge)
def process(challenge) case (@state ||= :initial_client_response) when :initial_client_response initial_client_response.tap { @state = :server_first_message } when :server_first_message recv_server_first_message challenge final_message_with_proof.tap { @state = :server_final_message } when :server_final_message recv_server_final_message challenge "".tap { @state = :done } else raise Error, "server sent after complete, %p" % [challenge] end rescue Exception => ex @state = ex raise end
def recv_server_final_message(server_final_message)
def recv_server_final_message(server_final_message) sparams = parse_challenge server_final_message @server_error = sparams["e"] and raise Error, "server error: %s" % [server_error] verifier = sparams["v"].unpack1("m") or raise Error, "server did not send verifier" verifier == server_signature or raise Error, "server verify failed: %p != %p" % [ server_signature, verifier ] end
def recv_server_first_message(server_first_message)
def recv_server_first_message(server_first_message) @server_first_message = server_first_message sparams = parse_challenge server_first_message @snonce = sparams["r"] or raise Error, "server did not send nonce" @salt = sparams["s"]&.unpack1("m") or raise Error, "server did not send salt" @iterations = sparams["i"]&.then {|i| Integer i } or raise Error, "server did not send iteration count" min_iterations <= iterations or raise Error, "too few iterations: %d" % [iterations] mext = sparams["m"] and raise Error, "mandatory extension: %p" % [mext] snonce.start_with? cnonce or raise Error, "invalid server nonce" end