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');
9 var AuthSession = function(db, username, password) {
11 this.username = username;
12 this.password = password;
15 AuthSession.prototype.equal = function(session) {
16 return session.db == this.db
17 && session.username == this.username
18 && session.password == this.password;
24 * Creates a new ScramSHA1 authentication mechanism
26 * @return {ScramSHA1} A cursor instance
28 var ScramSHA1 = function(bson) {
34 var parsePayload = function(payload) {
36 var parts = payload.split(',');
38 for(var i = 0; i < parts.length; i++) {
39 var valueParts = parts[i].split('=');
40 dict[valueParts[0]] = valueParts[1];
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');
58 var xor = function(a, b) {
59 if (!Buffer.isBuffer(a)) a = new Buffer(a)
60 if (!Buffer.isBuffer(b)) b = new Buffer(b)
62 if (a.length > b.length) {
63 for (var i = 0; i < b.length; i++) {
67 for (var i = 0; i < a.length; i++) {
71 return new Buffer(res);
74 // Create a final digest
75 var hi = function(data, salt, iterations) {
77 var digest = function(msg) {
78 var hmac = crypto.createHmac('sha1', data);
80 return new Buffer(hmac.digest('base64'), 'base64');
84 salt = Buffer.concat([salt, new Buffer('\x00\x00\x00\x01')])
85 var ui = digest(salt);
88 for(var i = 0; i < iterations - 1; i++) {
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
107 ScramSHA1.prototype.auth = function(server, connections, db, username, password, callback) {
110 var count = connections.length;
111 if(count == 0) return callback(null, null);
114 var numberOfValidConnections = 0;
115 var credentialsValid = false;
116 var errorObject = null;
119 var executeScram = function(connection) {
121 username = username.replace('=', "=3D").replace(',', '=2C');
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);
128 // Build command structure
131 , mechanism: 'SCRAM-SHA-1'
132 , payload: new Binary(f("n,,%s", firstBare))
137 var handleError = function(err, r) {
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;
146 credentialsValid = true;
147 numberOfValidConnections = numberOfValidConnections + 1;
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);
166 var handleEnd = function(_err, _r) {
168 handleError(_err, _r)
169 // Adjust the number of connections
171 // Execute the finish
172 finish(count, numberOfValidConnections);
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) {
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);
196 // Get the dictionary
197 var dict = parsePayload(r.result.payload.value())
200 var iterations = parseInt(dict.i, 10);
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')
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');
216 // Create the stored key
217 var hash = crypto.createHash('sha1');
218 hash.update(clientKey);
219 var storedKey = new Buffer(hash.digest('base64'), 'base64');
221 // Create the authentication message
222 var authMsg = [firstBare, r.result.payload.value().toString('base64'), withoutProof].join(',');
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');
229 // Create client proof
230 var clientProof = f("p=%s", new Buffer(xor(clientKey, clientSig)).toString('base64'));
232 // Create client final
233 var clientFinal = [withoutProof, clientProof].join(',');
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');
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');
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;
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) {
336 // Done re-authenticating
345 module.exports = ScramSHA1;