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

Inherits:
Object
  • Object
show all
Defined in:
lib/net/imap/sasl/digest_md5_authenticator.rb

Overview

Net::IMAP authenticator for the “‘DIGEST-MD5`” 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.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(user = nil, pass = nil, authz = nil, username: nil, password: nil, authzid: nil, authcid: nil, secret: nil, warn_deprecation: true) ⇒ DigestMD5Authenticator

:call-seq:

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” 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.



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# 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

#authzidObject (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"


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

def authzid
  @authzid
end

#passwordObject (readonly)

A password or passphrase that matches the #username.

The password will be used to create the response digest.



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

def password
  @password
end

#usernameObject (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.



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

def username
  @username
end

Instance Method Details

#done?Boolean

Returns:

  • (Boolean)


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

def done?; @stage == STAGE_DONE end

#initial_response?Boolean

Returns:

  • (Boolean)


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

def initial_response?; false end

#process(challenge) ⇒ Object

Responds to server challenge in two stages.



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# 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