/node_modules/mongoose/node_modules/mongodb-core/lib/auth/scram.js

https://bitbucket.org/coleman333/smartsite · JavaScript · 367 lines · 245 code · 48 blank · 74 comment · 42 complexity · ddb3f61fe4a8da150a8f1da36a07e4d3 MD5 · raw file

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