class HaystackRuby::Auth::Scram::Conversation

Attributes

auth_token[R]
nonce[R]
server_nonce[R]
server_salt[R]

Public Class Methods

new(user, url) click to toggle source
# File lib/haystack_ruby/auth/conversation.rb, line 10
def initialize(user, url)
  @user = user
  @url = url
  @nonce = SecureRandom.base64.tr('=','') #TODO check if problem to potentially strip =
  @digest = OpenSSL::Digest::SHA256.new 
  @handshake_token = Base64.strict_encode64(@user.username)
end

Public Instance Methods

authorize() click to toggle source
# File lib/haystack_ruby/auth/conversation.rb, line 18
def authorize
  res = send_first_message
  parse_first_response(res)
  res = send_second_message
  parse_second_response(res)
end
connection() click to toggle source
# File lib/haystack_ruby/auth/conversation.rb, line 25
def connection 
  @connection ||= Faraday.new(:url => @url) do |faraday|
    # faraday.response :logger                  # log requests to STDOUT
    faraday.adapter  Faraday.default_adapter  # make requests with Net::HTTP
    faraday.headers['Accept'] = 'application/json' #TODO enable more formats
    faraday.headers['Content-Type'] = 'text/plain'
  end
end
parse_first_response(response) click to toggle source

pull server data out of response to first message

# File lib/haystack_ruby/auth/conversation.rb, line 44
def parse_first_response(response)

  # parse server response to first message
  response_str = response.env.response_headers['www-authenticate']
  unless response_str.index('scram ') == 0
    throw 'Invalid response from server'
  end
  response_str.slice! 'scram '
  response_vars = {}
  response_str.split(', ').each do |pair|
    key,value = pair.split('=')
    response_vars[key] = value
  end
  unless response_vars['hash'] == 'SHA-256'
    throw "Server requested unsupported hash algorithm: #{response_vars['hash']}"
  end

  # todo check handshake token (should be base64 encode of username)

  @server_first_msg = Base64.decode64(response_vars["data"])
  server_vars = {}
  @server_first_msg.split(',').each do |pair|
    key,value = pair.split '='
    server_vars[key] = value
  end
  @server_nonce = server_vars['r']
  @server_salt = server_vars['s'] #Base64.decode64(server_vars['s'])
  @server_iterations = server_vars['i'].to_i
end
parse_second_response(response) click to toggle source
# File lib/haystack_ruby/auth/conversation.rb, line 81
def parse_second_response(response)
  begin
    response_str = response.env.response_headers['authentication-info']
    response_vars = {}
    response_str.split(', ').each do |pair|
      key,value = pair.split('=')
      response_vars[key] = value
    end
    # decode data attribute to check server signature is as expected
    key,val = Base64.decode64(response_vars['data']).split('=')
    response_vars[key] = val
    server_sig = response_vars['v']
    unless server_sig == expected_server_signature
      throw "invalid signature from server"
    end
    @auth_token = response_vars['authToken']
  # rescue Exception => e
  #   raise
  end
  
end
send_first_message() click to toggle source

first message sent by client to server

# File lib/haystack_ruby/auth/conversation.rb, line 35
def send_first_message
  res = connection.get('about') do |req|
    req.headers['Authorization'] = "SCRAM handshakeToken=#{@handshake_token},data=#{Base64.urlsafe_encode64(first_message).tr('=','')}"
  end
  res
  
end
send_second_message() click to toggle source
# File lib/haystack_ruby/auth/conversation.rb, line 74
def send_second_message
  res = connection.get('about') do |req|
    req.headers['Authorization'] = "SCRAM handshakeToken=#{@handshake_token},data=#{Base64.strict_encode64(client_final).tr('=','')}"
  end
  res
  
end
test_auth_token() click to toggle source
# File lib/haystack_ruby/auth/conversation.rb, line 103
def test_auth_token
  res = connection.get('about') do |req|
    req.headers['Authorization'] = "BEARER authToken=#{@auth_token}"
  end
end

Private Instance Methods

auth_message() click to toggle source

utility methods, closely matched with SCRAM notation and algorithm overview here: tools.ietf.org/html/rfc5802#section-3

# File lib/haystack_ruby/auth/conversation.rb, line 114
def auth_message
  @auth_message ||= "#{first_message},#{@server_first_msg},#{without_proof}"
end
client_final() click to toggle source
# File lib/haystack_ruby/auth/conversation.rb, line 118
def client_final
  @client_final ||= "#{without_proof},p=" +
    client_proof(client_key, client_signature(stored_key(client_key), auth_message))
end
client_key() click to toggle source
# File lib/haystack_ruby/auth/conversation.rb, line 123
def client_key
  @client_key ||= hmac(salted_password, 'Client Key')
end
client_proof(key, signature) click to toggle source
# File lib/haystack_ruby/auth/conversation.rb, line 127
def client_proof(key, signature)
  @client_proof ||= Base64.strict_encode64(xor(key, signature))
end
client_signature(key, message) click to toggle source
# File lib/haystack_ruby/auth/conversation.rb, line 131
def client_signature(key, message)
  @client_signature ||= hmac(key, message)
end
expected_server_key() click to toggle source
# File lib/haystack_ruby/auth/conversation.rb, line 135
def expected_server_key
  @server_key ||= hmac(salted_password, 'Server Key')
end
expected_server_signature() click to toggle source
# File lib/haystack_ruby/auth/conversation.rb, line 139
def expected_server_signature
  @server_signature ||= Base64.strict_encode64(hmac(expected_server_key, auth_message)).tr('=','')
end
first_message() click to toggle source
# File lib/haystack_ruby/auth/conversation.rb, line 143
def first_message
  "n=#{@user.username},r=#{@nonce}"
end
h(string) click to toggle source
# File lib/haystack_ruby/auth/conversation.rb, line 147
def h(string)
  @digest.digest(string)
end
hi(data) click to toggle source
# File lib/haystack_ruby/auth/conversation.rb, line 151
def hi(data)
  OpenSSL::PKCS5.pbkdf2_hmac(
    data, 
    Base64.decode64(@server_salt), 
    @server_iterations, 
    @digest.digest_length,
    @digest
  )
end
hmac(data, key) click to toggle source
# File lib/haystack_ruby/auth/conversation.rb, line 161
def hmac(data, key)
  OpenSSL::HMAC.digest(@digest,data,key)
end
salted_password() click to toggle source
# File lib/haystack_ruby/auth/conversation.rb, line 166
def salted_password
  @salted_password ||= hi(@user.password)
end
stored_key(key) click to toggle source
# File lib/haystack_ruby/auth/conversation.rb, line 170
def stored_key(key)
  h(key)
end
without_proof() click to toggle source
# File lib/haystack_ruby/auth/conversation.rb, line 174
def without_proof
  @without_proof ||= "c=biws,r=#{@server_nonce}"
end
xor(first, second) click to toggle source
# File lib/haystack_ruby/auth/conversation.rb, line 178
def xor(first, second)
  first.bytes.zip(second.bytes).map{ |(a,b)| (a ^ b).chr }.join('')
end