Class: EM::Mongo::SCRAM

Inherits:
Authentication show all
Defined in:
lib/em-mongo/auth/scram.rb

Overview

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

Defined Under Namespace

Classes: FirstMessage

Constant Summary collapse

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

Constants inherited from Authentication

Authentication::SYSTEM_COMMAND_COLLECTION

Instance Method Summary collapse

Methods inherited from Authentication

#initialize

Constructor Details

This class inherits a constructor from EM::Mongo::Authentication

Instance Method Details

#authenticate(username, password) ⇒ EM::Mongo::RequestResponse

Returns Calls back with true or false, indicating success or failure.



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
# File 'lib/em-mongo/auth/scram.rb', line 40

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

#client_keyObject



214
215
216
# File 'lib/em-mongo/auth/scram.rb', line 214

def client_key 
  @client_key ||= hmac(salted_password,CLIENT_KEY)
end

#client_nonceObject



199
200
201
# File 'lib/em-mongo/auth/scram.rb', line 199

def client_nonce
    @client_nonce ||= SecureRandom.base64
end

#client_signatureObject

needs @username, @plain_password, @salt, @iterations, @auth_message defined



223
224
225
# File 'lib/em-mongo/auth/scram.rb', line 223

def client_signature
  @client_signature ||= hmac(DIGEST.digest(client_key), @auth_message)
end

#handle_server_end(response, conv_id) ⇒ Object

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


125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/em-mongo/auth/scram.rb', line 125

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

#hashed_passwordObject

needs @username, @plain_password defined



204
205
206
# File 'lib/em-mongo/auth/scram.rb', line 204

def hashed_password
  @hashed_password ||= Support.hash_password(@username, @plain_password).encode("UTF-8")
end

#hi(password, salt, iterations) ⇒ Object



177
178
179
180
181
182
183
184
# File 'lib/em-mongo/auth/scram.rb', line 177

def hi(password, salt, iterations)
  OpenSSL::PKCS5.pbkdf2_hmac_sha1(
    password,
    Base64.strict_decode64(salt),
    iterations,
    DIGEST.size
   )
end

#hmac(data, key) ⇒ Object



186
187
188
# File 'lib/em-mongo/auth/scram.rb', line 186

def hmac(data,key)
  OpenSSL::HMAC.digest(DIGEST, data, key)
end

#is_server_response_valid?(response) ⇒ Boolean

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


151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/em-mongo/auth/scram.rb', line 151

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

#salted_passwordObject

needs @username, @plain_password, @salt, @iterations defined



209
210
211
# File 'lib/em-mongo/auth/scram.rb', line 209

def salted_password
  @salted_password ||= hi(hashed_password, @salt, @iterations)
end

#server_keyObject

server_key = hmac(salted_password,“Server Key”)



218
219
220
# File 'lib/em-mongo/auth/scram.rb', line 218

def server_key
  @server_key ||= hmac(salted_password,SERVER_KEY)
end

#server_signatureObject

server_signature = B64(hmac(server_key, auth_message)



228
229
230
# File 'lib/em-mongo/auth/scram.rb', line 228

def server_signature
  @server_signature ||= Base64.strict_encode64(hmac(server_key, @auth_message))
end

#verifier_valid?(verifier) ⇒ Boolean

verify the verifier (v=…)



169
170
171
# File 'lib/em-mongo/auth/scram.rb', line 169

def verifier_valid?(verifier)
  verifier == server_signature
end

#xor(first, second) ⇒ Object

xor for strings



191
192
193
194
195
196
# File 'lib/em-mongo/auth/scram.rb', line 191

def xor(first, second)
  first.bytes
    .zip(second.bytes)
    .map{|(x,y)| (x ^ y).chr}
    .join('')
end