1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 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 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237
|
require 'openssl'
require 'bson'
require 'eventmachine'
require_relative '../support.rb'
module EM::Mongo
# an RFC 5802 compilant SCRAM(-SHA-1) implementation
# for MongoDB-Authentication
#
# so everything is encapsulated, but the main part (PAYLOAD of messages) is RFC5802 compilant
class SCRAM < Authentication
MECHANISM = 'SCRAM-SHA-1'.freeze
DIGEST = OpenSSL::Digest::SHA1.new.freeze
CLIENT_FIRST_MESSAGE = { saslStart: 1, autoAuthorize: 1 }.freeze
CLIENT_FINAL_MESSAGE = CLIENT_EMPTY_MESSAGE = { saslContinue: 1 }.freeze
CLIENT_KEY = 'Client Key'.freeze
SERVER_KEY = 'Server Key'.freeze
RNONCE = /r=([^,]*)/.freeze
SALT = /s=([^,]*)/.freeze
ITERATIONS = /i=(\d+)/.freeze
VERIFIER = /v=([^,]*)/.freeze
PAYLOAD = 'payload'.freeze
# @param [String] username
# @param [String] password
#
# @return [EM::Mongo::RequestResponse] Calls back with +true+ or +false+, indicating success or failure
#
# @raise [AuthenticationError]
#
# @core authenticate authenticate-instance_method
def authenticate(username, password)
response = RequestResponse.new
#TODO look for fail-fast-ness (strange word!?)
#TODO Flatten Hierarchies
@username = username
@plain_password = password
gs2_header = 'n,,'
client_first_bare = "n=#{@username},r=#{client_nonce}"
client_first = BSON::Binary.new(gs2_header+client_first_bare) # client_first msg
client_first_msg = CLIENT_FIRST_MESSAGE.merge({PAYLOAD=>client_first, mechanism:MECHANISM})
client_first_resp = @db.collection(EM::Mongo::Database::SYSTEM_COMMAND_COLLECTION).first(client_first_msg) #TODO extract and make easier to understand (e.g. command(first_msg) or sthg like that)
#server_first_resp #for flattening
client_first_resp.callback do |res_first|
if not is_server_response_valid? res_first
response.fail "first server response not valid: " + res_first.to_s
else
# take the salt & iterations and do the pw-derivation
server_first = res_first[PAYLOAD].to_s
@conversation_id=conv_id = res_first['conversationId']
combined_nonce = server_first.match(RNONCE)[1] #r= ...
salt = server_first.match( SALT )[1] #s=... (from server_first)
iterations = server_first.match(ITERATIONS)[1].to_i #i=... ..
if not combined_nonce.start_with?(client_nonce) # combined_nonce should be client_nonce+server_nonce
response.fail "nonce doesn't start with client_nonce: " + res_first.to_s
else
client_final_wo_proof= "c=#{Base64.strict_encode64(gs2_header)},r=#{combined_nonce}" #c='biws'
auth_message = client_first_bare + ',' + server_first + ',' + client_final_wo_proof
# proof = clientKey XOR clientSig ## needs to be sent back
#
# ClientSign = HMAC(StoredKey, AuthMessage)
# StoredKey = H(ClientKey) ## lt. RFC5802 (needs to be verified against ruby-mongo driver impl)
# AuthMessage = client_first_bare + ','+server_first+','+client_final_wo_proof
@salt = salt
@iterations = iterations
#client_key = client_key()
@auth_message = auth_message
#client_signature = client_signature()
proof = Base64.strict_encode64(xor(client_key, client_signature))
client_final = BSON::Binary.new ( client_final_wo_proof + ",p=#{proof}")
client_final_msg = CLIENT_FINAL_MESSAGE.merge({PAYLOAD => client_final, conversationId: conv_id})
client_final_resp = @db.collection(SYSTEM_COMMAND_COLLECTION).first(client_final_msg)
client_final_resp.callback do |res_final|
if not is_server_response_valid? res_final
response.fail "Final Server Response not valid " + res_final.to_s
else
server_final = res_final[PAYLOAD].to_s # in RFC this equals server_final
verifier = server_final.match(VERIFIER)[1] #r= ...
if verifier and verifier_valid? verifier
handle_server_end(response,conv_id) # will set the response
else
response.fail "verifier #{verifier.nil? ? 'not present':'invalid'} #{res_final}"
end
end
end
client_final_resp.errback { |err| response.fail err }
end
end
end
client_first_resp.errback {
|err| response.fail err }
return response
end
# MongoDB handles the end of authentication different than in RFC 5802
# it needs at least an additional empty response (this needs to be iterated until res[done]=true
# (at least it is done so in the official mongo-ruby-drive (at least it is done so in the official mongo-ruby-driver))
# -> recursion (is technically more loop than recursion but here it's one)
#
# @param response [EM::Mongo::ResponseRequest] to fail or succeed after completion
# @param conv_id ConversationId to send to the server on each iteration
def handle_server_end(response,conv_id) # will set the response
client_end = BSON::Binary.new('')
client_end_msg = CLIENT_EMPTY_MESSAGE.merge(PAYLOAD=>client_end, conversationId:conv_id)
server_end_resp = @db.collection(SYSTEM_COMMAND_COLLECTION).first(client_end_msg)
server_end_resp.errback{|err| response.fail err}
server_end_resp.callback do |res|
if not is_server_response_valid? res
response.fail "got invalid response on handling server_end: #{res.nil? ? 'nil' : res}"
else
if res['done'] == true || res['done'] == 'true'
response.succeed true
else
handle_server_end(response,conv_id) # try it again
end
end
end
end
# to be valid the response has to
# * be not nil
# * contain at least ['done'], ['ok'], ['payload'], ['conversationId']
# * ['ok'].to_i has to be 1
# * ['conversationId'] has to match the first sent one
# @param [BSON::OrderedHash] response the response got from server
def is_server_response_valid?(response)
if response.nil? then return false; end
if response['done'].nil? or
response['ok'].nil? or
response['payload'].nil? or
response['conversationId'].nil? then
return false;
end
if not Support.ok? response then return false; end
if not @conversation_id.nil? and response['conversationId'] != @conversation_id
return false;
end
true
end
## verify the verifier (v=...)
def verifier_valid?(verifier)
verifier == server_signature
end
### Building blocks
# @see http://tools.ietf.org/html/rfc5802#section-2.2
def hi(password, salt, iterations)
OpenSSL::PKCS5.pbkdf2_hmac_sha1(
password,
Base64.strict_decode64(salt),
iterations,
DIGEST.size
)
end
def hmac(data,key)
OpenSSL::HMAC.digest(DIGEST, data, key)
end
# xor for strings
def xor(first, second)
first.bytes
.zip(second.bytes)
.map{|(x,y)| (x ^ y).chr}
.join('')
end
def client_nonce
@client_nonce ||= SecureRandom.base64
end
# needs @username, @plain_password defined
def hashed_password
@hashed_password ||= Support.hash_password(@username, @plain_password).encode("UTF-8")
end
#needs @username, @plain_password, @salt, @iterations defined
def salted_password
@salted_password ||= hi(hashed_password, @salt, @iterations)
end
# @see http://tools.ietf.org/html/rfc5802#section-3
def client_key
@client_key ||= hmac(salted_password,CLIENT_KEY)
end
# server_key = hmac(salted_password,"Server Key")
def server_key
@server_key ||= hmac(salted_password,SERVER_KEY)
end
#needs @username, @plain_password, @salt, @iterations, @auth_message defined
def client_signature
@client_signature ||= hmac(DIGEST.digest(client_key), @auth_message)
end
# server_signature = B64(hmac(server_key, auth_message)
def server_signature
@server_signature ||= Base64.strict_encode64(hmac(server_key, @auth_message))
end
class FirstMessage
include EM::Deferrable
end
end
end
|