123456789_123456789_123456789_123456789_123456789_

Class: Net::IMAP::SASL::DigestMD5Authenticator

Relationships & Source Files
Inherits: Object
Defined in: lib/net/imap/sasl/digest_md5_authenticator.rb

Overview

::Net::IMAP authenticator for the “‘DIGEST-MD5`” ::Net::IMAP::SASL mechanism type, specified in RFC-2831. See Net::IMAP#authenticate.

Deprecated

DIGEST-MD5” has been deprecated by RFC-6331 and should not be relied on for security. It is included for compatibility with existing servers.

Constant Summary

Class Method Summary

Instance Attribute Summary

Instance Method Summary

Constructor Details

.new(username, password, authzid = nil, **options) ⇒ authenticator .new(username:, password:, authzid: nil, **options) ⇒ authenticator .new(authcid:, password:, authzid: nil, **options) ⇒ authenticator

Creates an Authenticator for the “DIGEST-MD5::Net::IMAP::SASL mechanism.

Called by Net::IMAP#authenticate and similar methods on other clients.

Parameters

  • #authcid ― Authentication identity that is associated with #password.

    #username ― An alias for authcid.

  • #password ― A password or passphrase associated with this #authcid.

  • optional #authzid ― Authorization identity to act as or on behalf of.

    When authzid is not set, the server should derive the authorization identity from the authentication identity.

  • optional warn_deprecation — Set to false to silence the warning.

Any other keyword arguments are silently ignored.

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 70

def initialize(user = nil, pass = nil, authz = nil,
               username: nil, password: nil, authzid: nil,
               authcid: nil, secret: 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."
    # TODO: recommend SCRAM instead.
  end
  require "digest/md5"
  require "strscan"
  @username, @password, @authzid = username, password, authzid
  @nc, @stage = {}, STAGE_ONE
end

Instance Attribute Details

#authcid (readonly)

Alias for #username.

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 25

alias authcid username

#authzid (readonly)

Authorization identity: an identity to act as or on behalf of. The identity form is application protocol specific. If not provided or left blank, the server derives an authorization identity from the authentication identity. The server is responsible for verifying the client’s credentials and verifying that the identity it associates with the client’s authentication identity is allowed to act as (or on behalf of) the authorization identity.

For example, an administrator or superuser might take on another role:

imap.authenticate "DIGEST-MD5", "root", ->{passwd}, authzid: "user"
[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 43

attr_reader :authzid

#done?Boolean (readonly)

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 156

def done?; @stage == STAGE_DONE end

#initial_response?Boolean (readonly)

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 88

def initial_response?; false end

#password (readonly)

A password or passphrase that matches the #username.

The password will be used to create the response digest.

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 30

attr_reader :password

#username (readonly) Also known as: #authcid

Authentication identity: the identity that matches the #password.

RFC-2831 uses the term username. “Authentication identity” is the generic term used by RFC-4422. RFC-4616 and many later RFCs abbreviate this to #authcid.

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 24

attr_reader :username

Instance Method Details

#nc(nonce) (private)

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 160

def nc(nonce)
  if @nc.has_key? nonce
    @nc[nonce] = @nc[nonce] + 1
  else
    @nc[nonce] = 1
  end
  return @nc[nonce]
end

#process(challenge)

Responds to server challenge in two stages.

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 91

def process(challenge)
  case @stage
  when STAGE_ONE
    @stage = STAGE_TWO
    sparams = {}
    c = StringScanner.new(challenge)
    while c.scan(/(?:\s*,)?\s*(\w)=("(?:[^\\"]|\\.)*"|[^,])\s*/)
      k, v = c[1], c[2]
      if v =~ /^"(.*)"$/
        v = $1
        if v =~ /,/
          v = v.split(',')
        end
      end
      sparams[k] = v
    end

    raise Net::IMAP::DataFormatError, "Bad Challenge: '#{challenge}'" unless c.eos? and sparams['qop']
    raise Net::IMAP::Error, "Server does not support auth (qop = #{sparams['qop'].join(',')})" unless sparams['qop'].include?("auth")

    response = {
      :nonce => sparams['nonce'],
      :username => @username,
      :realm => sparams['realm'],
      :cnonce => Digest::MD5.hexdigest("%.15f:%.15f:%d" % [Time.now.to_f, rand, Process.pid.to_s]),
      :'digest-uri' => 'imap/' + sparams['realm'],
      :qop => 'auth',
      :maxbuf => 65535,
      :nc => "%08d" % nc(sparams['nonce']),
      :charset => sparams['charset'],
    }

    response[:authzid] = @authzid unless @authzid.nil?

    # now, the real thing
    a0 = Digest::MD5.digest( [ response.values_at(:username, :realm), @password ].join(':') )

    a1 = [ a0, response.values_at(:nonce,:cnonce) ].join(':')
    a1 << ':' + response[:authzid] unless response[:authzid].nil?

    a2 = "AUTHENTICATE:" + response[:'digest-uri']
    a2 << ":00000000000000000000000000000000" if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/

    response[:response] = Digest::MD5.hexdigest(
      [
        Digest::MD5.hexdigest(a1),
        response.values_at(:nonce, :nc, :cnonce, :qop),
        Digest::MD5.hexdigest(a2)
      ].join(':')
    )

    return response.keys.map {|key| qdval(key.to_s, response[key]) }.join(',')
  when STAGE_TWO
    @stage = STAGE_DONE
    # if at the second stage, return an empty string
    if challenge =~ /rspauth=/
      return ''
    else
      raise ResponseParseError, challenge
    end
  else
    raise ResponseParseError, challenge
  end
end

#qdval(k, v) (private)

some responses need quoting

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 170

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