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