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');
9 var BSON = retrieveBSON(),
12 var AuthSession = function(db, username, password) {
14 this.username = username;
15 this.password = password;
18 AuthSession.prototype.equal = function(session) {
19 return session.db == this.db
20 && session.username == this.username
21 && session.password == this.password;
27 * Creates a new ScramSHA1 authentication mechanism
29 * @return {ScramSHA1} A cursor instance
31 var ScramSHA1 = function(bson) {
37 var parsePayload = function(payload) {
39 var parts = payload.split(',');
41 for(var i = 0; i < parts.length; i++) {
42 var valueParts = parts[i].split('=');
43 dict[valueParts[0]] = valueParts[1];
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');
61 var xor = function(a, b) {
62 if (!Buffer.isBuffer(a)) a = new Buffer(a)
63 if (!Buffer.isBuffer(b)) b = new Buffer(b)
65 if (a.length > b.length) {
66 for (var i = 0; i < b.length; i++) {
70 for (i = 0; i < a.length; i++) {
74 return new Buffer(res);
77 // Create a final digest
78 var hi = function(data, salt, iterations) {
80 var digest = function(msg) {
81 var hmac = crypto.createHmac('sha1', data);
83 return new Buffer(hmac.digest('base64'), 'base64');
87 salt = Buffer.concat([salt, new Buffer('\x00\x00\x00\x01')])
88 var ui = digest(salt);
91 for(var i = 0; i < iterations - 1; i++) {
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
110 ScramSHA1.prototype.auth = function(server, connections, db, username, password, callback) {
113 var count = connections.length;
114 if(count == 0) return callback(null, null);
117 var numberOfValidConnections = 0;
118 var errorObject = null;
121 var executeScram = function(connection) {
123 username = username.replace('=', "=3D").replace(',', '=2C');
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);
130 // Build command structure
133 , mechanism: 'SCRAM-SHA-1'
134 , payload: new Binary(f("n,,%s", firstBare))
139 var handleError = function(err, r) {
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;
148 numberOfValidConnections = numberOfValidConnections + 1;
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);
167 var handleEnd = function(_err, _r) {
169 handleError(_err, _r)
170 // Adjust the number of connections
172 // Execute the finish
173 finish(count, numberOfValidConnections);
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) {
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);
197 // Get the dictionary
198 var dict = parsePayload(r.result.payload.value())
201 var iterations = parseInt(dict.i, 10);
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')
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');
217 // Create the stored key
218 var hash = crypto.createHash('sha1');
219 hash.update(clientKey);
220 var storedKey = new Buffer(hash.digest('base64'), 'base64');
222 // Create the authentication message
223 var authMsg = [firstBare, r.result.payload.value().toString('base64'), withoutProof].join(',');
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');
230 // Create client proof
231 var clientProof = f("p=%s", new Buffer(xor(clientKey, clientSig)).toString('base64'));
233 // Create client final
234 var clientFinal = [withoutProof, clientProof].join(',');
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');
241 // Generate server signature
242 hmac = crypto.createHmac('sha1', serverKey);
243 hmac.update(new Buffer(authMsg))
246 // Create continue message
249 , conversationId: r.result.conversationId
250 , payload: new Binary(new Buffer(clientFinal))
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) {
262 , conversationId: r.result.conversationId
263 , payload: new Buffer(0)
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) {
279 var _execute = function(_connection) {
280 process.nextTick(function() {
281 executeScram(_connection);
285 // For each connection we need to authenticate
286 while(connections.length > 0) {
287 _execute(connections.shift());
291 // Add to store only if it does not exist
292 var addAuthSession = function(authStore, session) {
295 for(var i = 0; i < authStore.length; i++) {
296 if(authStore[i].equal(session)) {
302 if(!found) authStore.push(session);
306 * Remove authStore credentials
308 * @param {string} db Name of database we are removing authStore details about
311 ScramSHA1.prototype.logout = function(dbName) {
312 this.authStore = this.authStore.filter(function(x) {
313 return x.db != dbName;
318 * Re authenticate pool
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
325 ScramSHA1.prototype.reauthenticate = function(server, connections, callback) {
326 var authStore = this.authStore.slice(0);
327 var count = authStore.length;
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) {
334 // Done re-authenticating
343 module.exports = ScramSHA1;