class Jabber::SASL::DigestMD5

SASL DIGEST-MD5 authentication helper (RFC2831)

Public Class Methods

new(stream) click to toggle source

Sends the wished auth mechanism and wait for a challenge

(proceed with #auth)

Calls superclass method Jabber::SASL::Base.new
# File lib/xmpp4r/sasl.rb, line 99
def initialize(stream)
  super

  challenge = {}
  error = nil
  @stream.send(generate_auth('DIGEST-MD5')) { |reply|
    if reply.name == 'challenge' and reply.namespace == NS_SASL
      challenge = decode_challenge(reply.text)
    else
      error = reply.first_element(nil).name
    end
    true
  }
  raise error if error

  @nonce = challenge['nonce']
  @realm = challenge['realm']
end

Public Instance Methods

auth(password) click to toggle source
  • Send a response

  • Wait for the server's challenge (which aren't checked)

  • Send a blind response to the server's challenge

# File lib/xmpp4r/sasl.rb, line 170
def auth(password)
  response = {}
  response['nonce'] = @nonce
  response['charset'] = 'utf-8'
  response['username'] = @stream.jid.node
  response['realm'] = @realm || @stream.jid.domain
  response['cnonce'] = generate_nonce
  response['nc'] = '00000001'
  response['qop'] = 'auth'
  response['digest-uri'] = "xmpp/#{@stream.jid.domain}"
  response['response'] = response_value(@stream.jid.node, @stream.jid.domain, response['digest-uri'], password, @nonce, response['cnonce'], response['qop'], response['authzid'])
  response.each { |key,value|
    unless %w(nc qop response charset).include? key
      response[key] = "\"#{value}\""
    end
  }

  response_text = response.collect { |k,v| "#{k}=#{v}" }.join(',')
  Jabber::debuglog("SASL DIGEST-MD5 response:\n#{response_text}\n#{response.inspect}")

  r = REXML::Element.new('response')
  r.add_namespace NS_SASL
  r.text = Base64::encode64(response_text).gsub(/\s/, '')

  success_already = false
  error = nil
  @stream.send(r) { |reply|
    if reply.name == 'success'
      success_already = true
    elsif reply.name != 'challenge'
      error = reply.first_element(nil).name
    end
    true
  }

  return if success_already
  raise error if error

  # TODO: check the challenge from the server

  r.text = nil
  @stream.send(r) { |reply|
    if reply.name != 'success'
      error = reply.first_element(nil).name
    end
    true
  }

  raise error if error
end
decode_challenge(challenge) click to toggle source
# File lib/xmpp4r/sasl.rb, line 118
def decode_challenge(challenge)
  text = Base64::decode64(challenge)
  res = {}

  state = :key
  key = ''
  value = ''
  text.scan(/./) do |ch|
    if state == :key
      if ch == '='
        state = :value
      else
        key += ch
      end

    elsif state == :value
      if ch == ','
        # due to our home-made parsing of the challenge, the key could have
        # leading whitespace. strip it, or that would break jabberd2 support.
        key = key.strip
        res[key] = value
        key = ''
        value = ''
        state = :key
      elsif ch == '"' and value == ''
        state = :quote
      else
        value += ch
      end

    elsif state == :quote
      if ch == '"'
        state = :value
      else
        value += ch
      end
    end
  end
  # due to our home-made parsing of the challenge, the key could have
  # leading whitespace. strip it, or that would break jabberd2 support.
  key = key.strip
  res[key] = value unless key == ''

  Jabber::debuglog("SASL DIGEST-MD5 challenge:\n#{text}\n#{res.inspect}")

  res
end

Private Instance Methods

h(s) click to toggle source

Function from RFC2831

# File lib/xmpp4r/sasl.rb, line 225
def h(s); Digest::MD5.digest(s); end
hh(s) click to toggle source

Function from RFC2831

# File lib/xmpp4r/sasl.rb, line 228
def hh(s); Digest::MD5.hexdigest(s); end
response_value(username, realm, digest_uri, passwd, nonce, cnonce, qop, authzid) click to toggle source

Calculate the value for the response field

# File lib/xmpp4r/sasl.rb, line 232
def response_value(username, realm, digest_uri, passwd, nonce, cnonce, qop, authzid)
  a1_h = h("#{username}:#{realm}:#{passwd}")
  a1 = "#{a1_h}:#{nonce}:#{cnonce}"
  if authzid
    a1 += ":#{authzid}"
  end
  if qop == 'auth-int' || qop == 'auth-conf'
    a2 = "AUTHENTICATE:#{digest_uri}:00000000000000000000000000000000"
  else
    a2 = "AUTHENTICATE:#{digest_uri}"
  end
  hh("#{hh(a1)}:#{nonce}:00000001:#{cnonce}:#{qop}:#{hh(a2)}")
end