0a620b343479bf6e837e8f93f6d674e81a5d5f16
[aai/esr-gui.git] /
1 "use strict";
2
3 var f = require('util').format
4   , crypto = require('crypto')
5   , Query = require('../connection/commands').Query
6   , Binary = require('bson').Binary
7   , MongoError = require('../error');
8
9 var AuthSession = function(db, username, password) {
10   this.db = db;
11   this.username = username;
12   this.password = password;
13 }
14
15 AuthSession.prototype.equal = function(session) {
16   return session.db == this.db
17     && session.username == this.username
18     && session.password == this.password;
19 }
20
21 var id = 0;
22
23 /**
24  * Creates a new ScramSHA1 authentication mechanism
25  * @class
26  * @return {ScramSHA1} A cursor instance
27  */
28 var ScramSHA1 = function(bson) {
29   this.bson = bson;
30   this.authStore = [];
31   this.id = id++;
32 }
33
34 var parsePayload = function(payload) {
35   var dict = {};
36   var parts = payload.split(',');
37
38   for(var i = 0; i < parts.length; i++) {
39     var valueParts = parts[i].split('=');
40     dict[valueParts[0]] = valueParts[1];
41   }
42
43   return dict;
44 }
45
46 var passwordDigest = function(username, password) {
47   if(typeof username != 'string') throw new MongoError("username must be a string");
48   if(typeof password != 'string') throw new MongoError("password must be a string");
49   if(password.length == 0) throw new MongoError("password cannot be empty");
50   // Use node md5 generator
51   var md5 = crypto.createHash('md5');
52   // Generate keys used for authentication
53   md5.update(username + ":mongo:" + password, 'utf8');
54   return md5.digest('hex');
55 }
56
57 // XOR two buffers
58 var xor = function(a, b) {
59   if (!Buffer.isBuffer(a)) a = new Buffer(a)
60   if (!Buffer.isBuffer(b)) b = new Buffer(b)
61   var res = []
62   if (a.length > b.length) {
63     for (var i = 0; i < b.length; i++) {
64       res.push(a[i] ^ b[i])
65     }
66   } else {
67     for (var i = 0; i < a.length; i++) {
68       res.push(a[i] ^ b[i])
69     }
70   }
71   return new Buffer(res);
72 }
73
74 // Create a final digest
75 var hi = function(data, salt, iterations) {
76   // Create digest
77   var digest = function(msg) {
78     var hmac = crypto.createHmac('sha1', data);
79     hmac.update(msg);
80     return new Buffer(hmac.digest('base64'), 'base64');
81   }
82
83   // Create variables
84   salt = Buffer.concat([salt, new Buffer('\x00\x00\x00\x01')])
85   var ui = digest(salt);
86   var u1 = ui;
87
88   for(var i = 0; i < iterations - 1; i++) {
89     u1 = digest(u1);
90     ui = xor(ui, u1);
91   }
92
93   return ui;
94 }
95
96 /**
97  * Authenticate
98  * @method
99  * @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
100  * @param {[]Connections} connections Connections to authenticate using this authenticator
101  * @param {string} db Name of the database
102  * @param {string} username Username
103  * @param {string} password Password
104  * @param {authResultCallback} callback The callback to return the result from the authentication
105  * @return {object}
106  */
107 ScramSHA1.prototype.auth = function(server, connections, db, username, password, callback) {
108   var self = this;
109   // Total connections
110   var count = connections.length;
111   if(count == 0) return callback(null, null);
112
113   // Valid connections
114   var numberOfValidConnections = 0;
115   var credentialsValid = false;
116   var errorObject = null;
117
118   // Execute MongoCR
119   var executeScram = function(connection) {
120     // Clean up the user
121     username = username.replace('=', "=3D").replace(',', '=2C');
122
123     // Create a random nonce
124     var nonce = crypto.randomBytes(24).toString('base64');
125     // var nonce = 'MsQUY9iw0T9fx2MUEz6LZPwGuhVvWAhc'
126     var firstBare = f("n=%s,r=%s", username, nonce);
127
128     // Build command structure
129     var cmd = {
130         saslStart: 1
131       , mechanism: 'SCRAM-SHA-1'
132       , payload: new Binary(f("n,,%s", firstBare))
133       , autoAuthorize: 1
134     }
135
136     // Handle the error
137     var handleError = function(err, r) {
138       if(err) {
139         numberOfValidConnections = numberOfValidConnections - 1;
140         errorObject = err; return false;
141       } else if(r.result['$err']) {
142         errorObject = r.result; return false;
143       } else if(r.result['errmsg']) {
144         errorObject = r.result; return false;
145       } else {
146         credentialsValid = true;
147         numberOfValidConnections = numberOfValidConnections + 1;
148       }
149
150       return true
151     }
152
153     // Finish up
154     var finish = function(_count, _numberOfValidConnections) {
155       if(_count == 0 && _numberOfValidConnections > 0) {
156         // Store the auth details
157         addAuthSession(self.authStore, new AuthSession(db, username, password));
158         // Return correct authentication
159         return callback(null, true);
160       } else if(_count == 0) {
161         if(errorObject == null) errorObject = new MongoError(f("failed to authenticate using scram"));
162         return callback(errorObject, false);
163       }
164     }
165
166     var handleEnd = function(_err, _r) {
167       // Handle any error
168       handleError(_err, _r)
169       // Adjust the number of connections
170       count = count - 1;
171       // Execute the finish
172       finish(count, numberOfValidConnections);
173     }
174
175     // Write the commmand on the connection
176     server(connection, new Query(self.bson, f("%s.$cmd", db), cmd, {
177       numberToSkip: 0, numberToReturn: 1
178     }), function(err, r) {
179       // Do we have an error, handle it
180       if(handleError(err, r) == false) {
181         count = count - 1;
182
183         if(count == 0 && numberOfValidConnections > 0) {
184           // Store the auth details
185           addAuthSession(self.authStore, new AuthSession(db, username, password));
186           // Return correct authentication
187           return callback(null, true);
188         } else if(count == 0) {
189           if(errorObject == null) errorObject = new MongoError(f("failed to authenticate using scram"));
190           return callback(errorObject, false);
191         }
192
193         return;
194       }
195
196       // Get the dictionary
197       var dict = parsePayload(r.result.payload.value())
198
199       // Unpack dictionary
200       var iterations = parseInt(dict.i, 10);
201       var salt = dict.s;
202       var rnonce = dict.r;
203
204       // Set up start of proof
205       var withoutProof = f("c=biws,r=%s", rnonce);
206       var passwordDig = passwordDigest(username, password);
207       var saltedPassword = hi(passwordDig
208           , new Buffer(salt, 'base64')
209           , iterations);
210
211       // Create the client key
212       var hmac = crypto.createHmac('sha1', saltedPassword);
213       hmac.update(new Buffer("Client Key"));
214       var clientKey = new Buffer(hmac.digest('base64'), 'base64');
215
216       // Create the stored key
217       var hash = crypto.createHash('sha1');
218       hash.update(clientKey);
219       var storedKey = new Buffer(hash.digest('base64'), 'base64');
220
221       // Create the authentication message
222       var authMsg = [firstBare, r.result.payload.value().toString('base64'), withoutProof].join(',');
223
224       // Create client signature
225       var hmac = crypto.createHmac('sha1', storedKey);
226       hmac.update(new Buffer(authMsg));
227       var clientSig = new Buffer(hmac.digest('base64'), 'base64');
228
229       // Create client proof
230       var clientProof = f("p=%s", new Buffer(xor(clientKey, clientSig)).toString('base64'));
231
232       // Create client final
233       var clientFinal = [withoutProof, clientProof].join(',');
234
235       // Generate server key
236       var hmac = crypto.createHmac('sha1', saltedPassword);
237       hmac.update(new Buffer('Server Key'))
238       var serverKey = new Buffer(hmac.digest('base64'), 'base64');
239
240       // Generate server signature
241       var hmac = crypto.createHmac('sha1', serverKey);
242       hmac.update(new Buffer(authMsg))
243       var serverSig = new Buffer(hmac.digest('base64'), 'base64');
244
245       //
246       // Create continue message
247       var cmd = {
248           saslContinue: 1
249         , conversationId: r.result.conversationId
250         , payload: new Binary(new Buffer(clientFinal))
251       }
252
253       //
254       // Execute sasl continue
255       // Write the commmand on the connection
256       server(connection, new Query(self.bson, f("%s.$cmd", db), cmd, {
257         numberToSkip: 0, numberToReturn: 1
258       }), function(err, r) {
259         if(r && r.result.done == false) {
260           var cmd = {
261               saslContinue: 1
262             , conversationId: r.result.conversationId
263             , payload: new Buffer(0)
264           }
265
266           // Write the commmand on the connection
267           server(connection, new Query(self.bson, f("%s.$cmd", db), cmd, {
268             numberToSkip: 0, numberToReturn: 1
269           }), function(err, r) {
270             handleEnd(err, r);
271           });
272         } else {
273           handleEnd(err, r);
274         }
275       });
276     });
277   }
278
279   var _execute = function(_connection) {
280     process.nextTick(function() {
281       executeScram(_connection);
282     });
283   }
284
285   // For each connection we need to authenticate
286   while(connections.length > 0) {
287     _execute(connections.shift());
288   }
289 }
290
291 // Add to store only if it does not exist
292 var addAuthSession = function(authStore, session) {
293   var found = false;
294
295   for(var i = 0; i < authStore.length; i++) {
296     if(authStore[i].equal(session)) {
297       found = true;
298       break;
299     }
300   }
301
302   if(!found) authStore.push(session);
303 }
304
305 /**
306  * Remove authStore credentials
307  * @method
308  * @param {string} db Name of database we are removing authStore details about
309  * @return {object}
310  */
311 ScramSHA1.prototype.logout = function(dbName) {
312   this.authStore = this.authStore.filter(function(x) {
313     return x.db != dbName;
314   });
315 }
316
317 /**
318  * Re authenticate pool
319  * @method
320  * @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
321  * @param {[]Connections} connections Connections to authenticate using this authenticator
322  * @param {authResultCallback} callback The callback to return the result from the authentication
323  * @return {object}
324  */
325 ScramSHA1.prototype.reauthenticate = function(server, connections, callback) {
326   var authStore = this.authStore.slice(0);
327   var count = authStore.length;
328   var err = null;
329   // No connections
330   if(count == 0) return callback(null, null);
331   // Iterate over all the auth details stored
332   for(var i = 0; i < authStore.length; i++) {
333     this.auth(server, connections, authStore[i].db, authStore[i].username, authStore[i].password, function(err, r) {
334       if(err) err = err;
335       count = count - 1;
336       // Done re-authenticating
337       if(count == 0) {
338         callback(err, null);
339       }
340     });
341   }
342 }
343
344
345 module.exports = ScramSHA1;