class Net::IMAP::SASL::DigestMD5Authenticator
security. It is included for compatibility with existing servers.<br>RFC-6331 and should not be relied on for
“DIGEST-MD5
” has been deprecated by
== Deprecated
in RFC-2831. See Net::IMAP#authenticate.
Net::IMAP authenticator for the DIGEST-MD5
SASL mechanism type, specified
def compute_a0(response)
def compute_a0(response) Digest::MD5.digest( [ response.values_at(:username, :realm), password ].join(":") ) end
def compute_a1(response)
def compute_a1(response) a0 = compute_a0(response) a1 = [ a0, response.values_at(:nonce, :cnonce) ].join(":") a1 << ":#{response[:authzid]}" unless response[:authzid].nil? a1 end
def compute_a2(response)
def compute_a2(response) a2 = "AUTHENTICATE:#{response[:"digest-uri"]}" if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/ a2 << ":00000000000000000000000000000000" end a2 end
def digest_uri
value of "ftp/ftp.example.com"; the SMTP server from the example above
example, the FTP service on "ftp.example.com" would have a "digest-uri"
to connect, formed from the serv-type, host, and serv-name. For
Indicates the principal name of the service with which the client wishes
>>>
From RFC-2831[https://www.rfc-editor.org/rfc/rfc2831]:
def digest_uri if service_name && service_name != host "#{service}/#{host}/#{service_name}" else "#{service}/#{host}" end end
def done?; @stage == STAGE_DONE end
def done?; @stage == STAGE_DONE end
def format_response(response)
def format_response(response) response.map {|k, v| qdval(k.to_s, v) }.join(",") end
def initial_response?; false end
def initial_response?; false end
def initialize(user = nil, pass = nil, authz = nil,
* _optional_ +warn_deprecation+ — Set to +false+ to silence the warning.
For Net::IMAP, this defaults to "imap".
"smtp", "ldap", "xmpp".
* _optional_ #service — the registered service protocol. E.g. "imap",
replicated.
* _optional_ #service_name — The generic host name when the server is
Defaults to #realm.
* _optional_ #host — FQDN for requested service.
Defaults to the last realm in the server-provided realms list.
* _optional_ #realm — A namespace for the #username, e.g. a domain.
identity from the authentication identity.
When +authzid+ is not set, the server should derive the authorization
* _optional_ #authzid ― Authorization identity to act as or on behalf of.
* #password ― A password or passphrase associated with this #authcid.
#username ― An alias for +authcid+.
* #authcid ― Authentication identity that is associated with #password.
==== Parameters
Called by Net::IMAP#authenticate and similar methods on other clients.
Creates an Authenticator for the "+DIGEST-MD5+" SASL mechanism.
new(authcid:, password:, authzid: nil, **options) -> authenticator
new(username:, password:, authzid: nil, **options) -> authenticator
new(username, password, authzid = nil, **options) -> authenticator
:call-seq:
def initialize(user = nil, pass = nil, authz = nil, username: nil, password: nil, authzid: nil, authcid: nil, secret: nil, realm: nil, service: "imap", host: nil, service_name: nil, warn_deprecation: true, **) username = authcid || username || user or raise ArgumentError, "missing username (authcid)" password ||= secret || pass or raise ArgumentError, "missing password" authzid ||= authz if warn_deprecation warn("WARNING: DIGEST-MD5 SASL mechanism was deprecated by RFC6331.", category: :deprecated) end require "digest/md5" require "securerandom" require "strscan" @username, @password, @authzid = username, password, authzid @realm = realm @host = host @service = service @service_name = service_name @nc, @stage = {}, STAGE_ONE end
def nc(nonce)
def nc(nonce) if @nc.has_key? nonce @nc[nonce] = @nc[nonce] + 1 else @nc[nonce] = 1 end end
def parse_challenge(challenge)
def parse_challenge(challenge) sparams = Hash.new {|h, k| h[k] = [] } c = StringScanner.new(challenge) c.skip LIST_DELIM while c.scan AUTH_PARAM k, v = c[1], c[2] k = k.downcase if v =~ /\A"(.*)"\z/mn v = $1.gsub(/\\(.)/mn, '\1') v = split_quoted_list(v, challenge) if QUOTED_LISTABLE.include? k end sparams[k] << v end if !c.eos? raise DataFormatError, "Unparsable challenge: %p" % [challenge] elsif sparams.empty? raise DataFormatError, "Empty challenge: %p" % [challenge] end sparams end
def process(challenge)
def process(challenge) case @stage when STAGE_ONE @stage = STAGE_TWO @sparams = parse_challenge(challenge) @qop = sparams.key?("qop") ? ["auth"] : sparams["qop"].flatten @nonce = sparams["nonce"] &.first @charset = sparams["charset"]&.first @realm ||= sparams["realm"] &.last @host ||= realm if !qop.include?("auth") raise DataFormatError, "Server does not support auth (qop = %p)" % [ sparams["qop"] ] elsif (emptykey = REQUIRED.find { sparams[_1].empty? }) raise DataFormatError, "Server didn't send %s (%p)" % [emptykey, challenge] elsif (multikey = NO_MULTIPLES.find { sparams[_1].length > 1 }) raise DataFormatError, "Server sent multiple %s (%p)" % [multikey, challenge] end response = { nonce: nonce, username: username, realm: realm, cnonce: SecureRandom.base64(32), "digest-uri": digest_uri, qop: "auth", maxbuf: 65535, nc: "%08d" % nc(nonce), charset: charset, } response[:authzid] = @authzid unless @authzid.nil? response[:response] = response_value(response) format_response(response) when STAGE_TWO @stage = STAGE_DONE raise ResponseParseError, challenge unless challenge =~ /rspauth=/ "" # if at the second stage, return an empty string else raise ResponseParseError, challenge end rescue => error @stage = error raise end
def qdval(k, v)
def qdval(k, v) return if k.nil? or v.nil? if %w"username authzid realm nonce cnonce digest-uri qop".include? k v = v.gsub(/([\\"])/, "\\\1") return '%s="%s"' % [k, v] else return '%s=%s' % [k, v] end end
def response_value(response)
def response_value(response) a1 = compute_a1(response) a2 = compute_a2(response) Digest::MD5.hexdigest( [ Digest::MD5.hexdigest(a1), response.values_at(:nonce, :nc, :cnonce, :qop), Digest::MD5.hexdigest(a2) ].join(":") ) end
def split_quoted_list(value, challenge)
def split_quoted_list(value, challenge) value.split(LIST_DELIM).reject(&:empty?).tap do _1.any? or raise DataFormatError, "Bad Challenge: %p" % [challenge] end end