cdc6ebb1afc0feabd72bbf36db8a3f525b9ea595
[aai/esr-gui.git] /
1 "use strict";
2
3 var f = require('util').format
4   , crypto = require('crypto')
5   , retrieveBSON = require('../connection/utils').retrieveBSON
6   , Query = require('../connection/commands').Query
7   , MongoError = require('../error');
8
9 var BSON = retrieveBSON(),
10   Binary = BSON.Binary;
11
12 var AuthSession = function(db, username, password) {
13   this.db = db;
14   this.username = username;
15   this.password = password;
16 }
17
18 AuthSession.prototype.equal = function(session) {
19   return session.db == this.db
20     && session.username == this.username
21     && session.password == this.password;
22 }
23
24 var id = 0;
25
26 /**
27  * Creates a new ScramSHA1 authentication mechanism
28  * @class
29  * @return {ScramSHA1} A cursor instance
30  */
31 var ScramSHA1 = function(bson) {
32   this.bson = bson;
33   this.authStore = [];
34   this.id = id++;
35 }
36
37 var parsePayload = function(payload) {
38   var dict = {};
39   var parts = payload.split(',');
40
41   for(var i = 0; i < parts.length; i++) {
42     var valueParts = parts[i].split('=');
43     dict[valueParts[0]] = valueParts[1];
44   }
45
46   return dict;
47 }
48
49 var passwordDigest = function(username, password) {
50   if(typeof username != 'string') throw new MongoError("username must be a string");
51   if(typeof password != 'string') throw new MongoError("password must be a string");
52   if(password.length == 0) throw new MongoError("password cannot be empty");
53   // Use node md5 generator
54   var md5 = crypto.createHash('md5');
55   // Generate keys used for authentication
56   md5.update(username + ":mongo:" + password, 'utf8');
57   return md5.digest('hex');
58 }
59
60 // XOR two buffers
61 var xor = function(a, b) {
62   if (!Buffer.isBuffer(a)) a = new Buffer(a)
63   if (!Buffer.isBuffer(b)) b = new Buffer(b)
64   var res = []
65   if (a.length > b.length) {
66     for (var i = 0; i < b.length; i++) {
67       res.push(a[i] ^ b[i])
68     }
69   } else {
70     for (i = 0; i < a.length; i++) {
71       res.push(a[i] ^ b[i])
72     }
73   }
74   return new Buffer(res);
75 }
76
77 // Create a final digest
78 var hi = function(data, salt, iterations) {
79   // Create digest
80   var digest = function(msg) {
81     var hmac = crypto.createHmac('sha1', data);
82     hmac.update(msg);
83     return new Buffer(hmac.digest('base64'), 'base64');
84   }
85
86   // Create variables
87   salt = Buffer.concat([salt, new Buffer('\x00\x00\x00\x01')])
88   var ui = digest(salt);
89   var u1 = ui;
90
91   for(var i = 0; i < iterations - 1; i++) {
92     u1 = digest(u1);
93     ui = xor(ui, u1);
94   }
95
96   return ui;
97 }
98
99 /**
100  * Authenticate
101  * @method
102  * @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
103  * @param {[]Connections} connections Connections to authenticate using this authenticator
104  * @param {string} db Name of the database
105  * @param {string} username Username
106  * @param {string} password Password
107  * @param {authResultCallback} callback The callback to return the result from the authentication
108  * @return {object}
109  */
110 ScramSHA1.prototype.auth = function(server, connections, db, username, password, callback) {
111   var self = this;
112   // Total connections
113   var count = connections.length;
114   if(count == 0) return callback(null, null);
115
116   // Valid connections
117   var numberOfValidConnections = 0;
118   var errorObject = null;
119
120   // Execute MongoCR
121   var executeScram = function(connection) {
122     // Clean up the user
123     username = username.replace('=', "=3D").replace(',', '=2C');
124
125     // Create a random nonce
126     var nonce = crypto.randomBytes(24).toString('base64');
127     // var nonce = 'MsQUY9iw0T9fx2MUEz6LZPwGuhVvWAhc'
128     var firstBare = f("n=%s,r=%s", username, nonce);
129
130     // Build command structure
131     var cmd = {
132         saslStart: 1
133       , mechanism: 'SCRAM-SHA-1'
134       , payload: new Binary(f("n,,%s", firstBare))
135       , autoAuthorize: 1
136     }
137
138     // Handle the error
139     var handleError = function(err, r) {
140       if(err) {
141         numberOfValidConnections = numberOfValidConnections - 1;
142         errorObject = err; return false;
143       } else if(r.result['$err']) {
144         errorObject = r.result; return false;
145       } else if(r.result['errmsg']) {
146         errorObject = r.result; return false;
147       } else {
148         numberOfValidConnections = numberOfValidConnections + 1;
149       }
150
151       return true
152     }
153
154     // Finish up
155     var finish = function(_count, _numberOfValidConnections) {
156       if(_count == 0 && _numberOfValidConnections > 0) {
157         // Store the auth details
158         addAuthSession(self.authStore, new AuthSession(db, username, password));
159         // Return correct authentication
160         return callback(null, true);
161       } else if(_count == 0) {
162         if(errorObject == null) errorObject = new MongoError(f("failed to authenticate using scram"));
163         return callback(errorObject, false);
164       }
165     }
166
167     var handleEnd = function(_err, _r) {
168       // Handle any error
169       handleError(_err, _r)
170       // Adjust the number of connections
171       count = count - 1;
172       // Execute the finish
173       finish(count, numberOfValidConnections);
174     }
175
176     // Write the commmand on the connection
177     server(connection, new Query(self.bson, f("%s.$cmd", db), cmd, {
178       numberToSkip: 0, numberToReturn: 1
179     }), function(err, r) {
180       // Do we have an error, handle it
181       if(handleError(err, r) == false) {
182         count = count - 1;
183
184         if(count == 0 && numberOfValidConnections > 0) {
185           // Store the auth details
186           addAuthSession(self.authStore, new AuthSession(db, username, password));
187           // Return correct authentication
188           return callback(null, true);
189         } else if(count == 0) {
190           if(errorObject == null) errorObject = new MongoError(f("failed to authenticate using scram"));
191           return callback(errorObject, false);
192         }
193
194         return;
195       }
196
197       // Get the dictionary
198       var dict = parsePayload(r.result.payload.value())
199
200       // Unpack dictionary
201       var iterations = parseInt(dict.i, 10);
202       var salt = dict.s;
203       var rnonce = dict.r;
204
205       // Set up start of proof
206       var withoutProof = f("c=biws,r=%s", rnonce);
207       var passwordDig = passwordDigest(username, password);
208       var saltedPassword = hi(passwordDig
209           , new Buffer(salt, 'base64')
210           , iterations);
211
212       // Create the client key
213       var hmac = crypto.createHmac('sha1', saltedPassword);
214       hmac.update(new Buffer("Client Key"));
215       var clientKey = new Buffer(hmac.digest('base64'), 'base64');
216
217       // Create the stored key
218       var hash = crypto.createHash('sha1');
219       hash.update(clientKey);
220       var storedKey = new Buffer(hash.digest('base64'), 'base64');
221
222       // Create the authentication message
223       var authMsg = [firstBare, r.result.payload.value().toString('base64'), withoutProof].join(',');
224
225       // Create client signature
226       hmac = crypto.createHmac('sha1', storedKey);
227       hmac.update(new Buffer(authMsg));
228       var clientSig = new Buffer(hmac.digest('base64'), 'base64');
229
230       // Create client proof
231       var clientProof = f("p=%s", new Buffer(xor(clientKey, clientSig)).toString('base64'));
232
233       // Create client final
234       var clientFinal = [withoutProof, clientProof].join(',');
235
236       // Generate server key
237       hmac = crypto.createHmac('sha1', saltedPassword);
238       hmac.update(new Buffer('Server Key'))
239       var serverKey = new Buffer(hmac.digest('base64'), 'base64');
240
241       // Generate server signature
242       hmac = crypto.createHmac('sha1', serverKey);
243       hmac.update(new Buffer(authMsg))
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   // No connections
329   if(count == 0) return callback(null, null);
330   // Iterate over all the auth details stored
331   for(var i = 0; i < authStore.length; i++) {
332     this.auth(server, connections, authStore[i].db, authStore[i].username, authStore[i].password, function(err) {
333       count = count - 1;
334       // Done re-authenticating
335       if(count == 0) {
336         callback(err, null);
337       }
338     });
339   }
340 }
341
342
343 module.exports = ScramSHA1;