Skip to content
This repository has been archived by the owner on Feb 4, 2022. It is now read-only.

Commit

Permalink
feat(auth): adds saslprep and SCRAM-SHA-256
Browse files Browse the repository at this point in the history
- Adds SCRAM-SHA-256 and SASLPrep support to auth.
- Throws an error if the iteration count in response to SASLStart is less than 4096.
- Implements Mechanism Negotiations for default authentication method
- Properly propagates server errors back to the user

Fixes NODE-1311
  • Loading branch information
daprahamian committed May 7, 2018
1 parent b0a3abb commit 506c087
Show file tree
Hide file tree
Showing 10 changed files with 342 additions and 104 deletions.
4 changes: 3 additions & 1 deletion index.js
Expand Up @@ -28,11 +28,13 @@ module.exports = {
// Raw operations
Query: require('./lib/connection/commands').Query,
// Auth mechanisms
defaultAuthProviders: require('./lib/auth/defaultAuthProviders').defaultAuthProviders,
MongoCR: require('./lib/auth/mongocr'),
X509: require('./lib/auth/x509'),
Plain: require('./lib/auth/plain'),
GSSAPI: require('./lib/auth/gssapi'),
ScramSHA1: require('./lib/auth/scram'),
ScramSHA1: require('./lib/auth/scram').ScramSHA1,
ScramSHA256: require('./lib/auth/scram').ScramSHA256,
// Utilities
parseConnectionString: require('./lib/uri_parser')
};
29 changes: 29 additions & 0 deletions lib/auth/defaultAuthProviders.js
@@ -0,0 +1,29 @@
'use strict';

const MongoCR = require('./mongocr');
const X509 = require('./x509');
const Plain = require('./plain');
const GSSAPI = require('./gssapi');
const SSPI = require('./sspi');
const ScramSHA1 = require('./scram').ScramSHA1;
const ScramSHA256 = require('./scram').ScramSHA256;

/**
* Returns the default authentication providers.
*
* @param {BSON} bson Bson definition
* @returns {Object} a mapping of auth names to auth types
*/
function defaultAuthProviders(bson) {
return {
mongocr: new MongoCR(bson),
x509: new X509(bson),
plain: new Plain(bson),
gssapi: new GSSAPI(bson),
sspi: new SSPI(bson),
'scram-sha-1': new ScramSHA1(bson),
'scram-sha-256': new ScramSHA256(bson)
};
}

module.exports = { defaultAuthProviders };
162 changes: 118 additions & 44 deletions lib/auth/scram.js
Expand Up @@ -6,6 +6,14 @@ var f = require('util').format,
Query = require('../connection/commands').Query,
MongoError = require('../error').MongoError;

let saslprep;

try {
saslprep = require('saslprep');
} catch (e) {
// don't do anything;
}

var BSON = retrieveBSON(),
Binary = BSON.Binary;

Expand All @@ -26,14 +34,15 @@ AuthSession.prototype.equal = function(session) {
var id = 0;

/**
* Creates a new ScramSHA1 authentication mechanism
* Creates a new ScramSHA authentication mechanism
* @class
* @return {ScramSHA1} A cursor instance
* @return {ScramSHA} A cursor instance
*/
var ScramSHA1 = function(bson) {
var ScramSHA = function(bson, cryptoMethod) {
this.bson = bson;
this.authStore = [];
this.id = id++;
this.cryptoMethod = cryptoMethod || 'sha1';
};

var parsePayload = function(payload) {
Expand All @@ -60,21 +69,32 @@ var passwordDigest = function(username, password) {
};

// XOR two buffers
var xor = function(a, b) {
function xor(a, b) {
if (!Buffer.isBuffer(a)) a = new Buffer(a);
if (!Buffer.isBuffer(b)) b = new Buffer(b);
var res = [];
if (a.length > b.length) {
for (var i = 0; i < b.length; i++) {
res.push(a[i] ^ b[i]);
}
} else {
for (i = 0; i < a.length; i++) {
res.push(a[i] ^ b[i]);
}
const length = Math.max(a.length, b.length);
const res = [];

for (let i = 0; i < length; i += 1) {
res.push(a[i] ^ b[i]);
}
return new Buffer(res);
};

return new Buffer(res).toString('base64');
}

function H(method, text) {
return crypto
.createHash(method)
.update(text)
.digest();
}

function HMAC(method, key, text) {
return crypto
.createHmac(method, key)
.update(text)
.digest();
}

var _hiCache = {};
var _hiCacheCount = 0;
Expand All @@ -83,15 +103,26 @@ var _hiCachePurge = function() {
_hiCacheCount = 0;
};

var hi = function(data, salt, iterations) {
const hiLengthMap = {
sha256: 32,
sha1: 20
};

function HI(data, salt, iterations, cryptoMethod) {
// omit the work if already generated
var key = [data, salt.toString('base64'), iterations].join('_');
const key = [data, salt.toString('base64'), iterations].join('_');
if (_hiCache[key] !== undefined) {
return _hiCache[key];
}

// generate the salt
var saltedData = crypto.pbkdf2Sync(data, salt, iterations, 20, 'sha1');
const saltedData = crypto.pbkdf2Sync(
data,
salt,
iterations,
hiLengthMap[cryptoMethod],
cryptoMethod
);

// cache a copy to speed up the next lookup, but prevent unbounded cache growth
if (_hiCacheCount >= 200) {
Expand All @@ -101,7 +132,7 @@ var hi = function(data, salt, iterations) {
_hiCache[key] = saltedData;
_hiCacheCount += 1;
return saltedData;
};
}

/**
* Authenticate
Expand All @@ -114,7 +145,7 @@ var hi = function(data, salt, iterations) {
* @param {authResultCallback} callback The callback to return the result from the authentication
* @return {object}
*/
ScramSHA1.prototype.auth = function(server, connections, db, username, password, callback) {
ScramSHA.prototype.auth = function(server, connections, db, username, password, callback) {
var self = this;
// Total connections
var count = connections.length;
Expand All @@ -124,6 +155,25 @@ ScramSHA1.prototype.auth = function(server, connections, db, username, password,
var numberOfValidConnections = 0;
var errorObject = null;

const cryptoMethod = this.cryptoMethod;
let mechanism = 'SCRAM-SHA-1';
let processedPassword;

if (cryptoMethod === 'sha256') {
mechanism = 'SCRAM-SHA-256';

let saslprepFn = (server.s && server.s.saslprep) || saslprep;

if (saslprepFn) {
processedPassword = saslprepFn(password);
} else {
console.warn('Warning: no saslprep library specified. Passwords will not be sanitized');
processedPassword = password;
}
} else {
processedPassword = passwordDigest(username, password);
}

// Execute MongoCR
var executeScram = function(connection) {
// Clean up the user
Expand All @@ -132,13 +182,21 @@ ScramSHA1.prototype.auth = function(server, connections, db, username, password,
// Create a random nonce
var nonce = crypto.randomBytes(24).toString('base64');
// var nonce = 'MsQUY9iw0T9fx2MUEz6LZPwGuhVvWAhc'
var firstBare = f('n=%s,r=%s', username, nonce);

// NOTE: This is done b/c Javascript uses UTF-16, but the server is hashing in UTF-8.
// Since the username is not sasl-prep-d, we need to do this here.
const firstBare = Buffer.concat([
Buffer.from('n=', 'utf8'),
Buffer.from(username, 'utf8'),
Buffer.from(',r=', 'utf8'),
Buffer.from(nonce, 'utf8')
]);

// Build command structure
var cmd = {
saslStart: 1,
mechanism: 'SCRAM-SHA-1',
payload: new Binary(f('n,,%s', firstBare)),
mechanism: mechanism,
payload: new Binary(Buffer.concat([Buffer.from('n,,', 'utf8'), firstBare])),
autoAuthorize: 1
};

Expand Down Expand Up @@ -220,38 +278,42 @@ ScramSHA1.prototype.auth = function(server, connections, db, username, password,

// Set up start of proof
var withoutProof = f('c=biws,r=%s', rnonce);
var passwordDig = passwordDigest(username, password);
var saltedPassword = hi(passwordDig, new Buffer(salt, 'base64'), iterations);
var saltedPassword = HI(
processedPassword,
new Buffer(salt, 'base64'),
iterations,
cryptoMethod
);

if (iterations && iterations < 4096) {
const error = new MongoError(`Server returned an invalid iteration count ${iterations}`);
return callback(error, false);
}

// Create the client key
var hmac = crypto.createHmac('sha1', saltedPassword);
hmac.update(new Buffer('Client Key'));
var clientKey = new Buffer(hmac.digest('base64'), 'base64');
const clientKey = HMAC(cryptoMethod, saltedPassword, 'Client Key');

// Create the stored key
var hash = crypto.createHash('sha1');
hash.update(clientKey);
var storedKey = new Buffer(hash.digest('base64'), 'base64');
const storedKey = H(cryptoMethod, clientKey);

// Create the authentication message
var authMsg = [firstBare, r.result.payload.value().toString('base64'), withoutProof].join(
','
);
const authMessage = [
firstBare,
r.result.payload.value().toString('base64'),
withoutProof
].join(',');

// Create client signature
hmac = crypto.createHmac('sha1', storedKey);
hmac.update(new Buffer(authMsg));
var clientSig = new Buffer(hmac.digest('base64'), 'base64');
const clientSignature = HMAC(cryptoMethod, storedKey, authMessage);

// Create client proof
var clientProof = f('p=%s', new Buffer(xor(clientKey, clientSig)).toString('base64'));
const clientProof = f('p=%s', xor(clientKey, clientSignature));

// Create client final
var clientFinal = [withoutProof, clientProof].join(',');
const clientFinal = [withoutProof, clientProof].join(',');

//
// Create continue message
var cmd = {
const cmd = {
saslContinue: 1,
conversationId: r.result.conversationId,
payload: new Binary(new Buffer(clientFinal))
Expand Down Expand Up @@ -326,7 +388,7 @@ var addAuthSession = function(authStore, session) {
* @param {string} db Name of database we are removing authStore details about
* @return {object}
*/
ScramSHA1.prototype.logout = function(dbName) {
ScramSHA.prototype.logout = function(dbName) {
this.authStore = this.authStore.filter(function(x) {
return x.db !== dbName;
});
Expand All @@ -340,7 +402,7 @@ ScramSHA1.prototype.logout = function(dbName) {
* @param {authResultCallback} callback The callback to return the result from the authentication
* @return {object}
*/
ScramSHA1.prototype.reauthenticate = function(server, connections, callback) {
ScramSHA.prototype.reauthenticate = function(server, connections, callback) {
var authStore = this.authStore.slice(0);
var count = authStore.length;
// No connections
Expand All @@ -364,4 +426,16 @@ ScramSHA1.prototype.reauthenticate = function(server, connections, callback) {
}
};

module.exports = ScramSHA1;
class ScramSHA1 extends ScramSHA {
constructor(bson) {
super(bson, 'sha1');
}
}

class ScramSHA256 extends ScramSHA {
constructor(bson) {
super(bson, 'sha256');
}
}

module.exports = { ScramSHA1, ScramSHA256 };
26 changes: 7 additions & 19 deletions lib/connection/pool.js
Expand Up @@ -18,12 +18,7 @@ var inherits = require('util').inherits,

const apm = require('./apm');

var MongoCR = require('../auth/mongocr'),
X509 = require('../auth/x509'),
Plain = require('../auth/plain'),
GSSAPI = require('../auth/gssapi'),
SSPI = require('../auth/sspi'),
ScramSHA1 = require('../auth/scram');
const defaultAuthProviders = require('../auth/defaultAuthProviders').defaultAuthProviders;

var DISCONNECTED = 'disconnected';
var CONNECTING = 'connecting';
Expand Down Expand Up @@ -147,14 +142,7 @@ var Pool = function(topology, options) {
this.queue = [];

// All the authProviders
this.authProviders = options.authProviders || {
mongocr: new MongoCR(options.bson),
x509: new X509(options.bson),
plain: new Plain(options.bson),
gssapi: new GSSAPI(options.bson),
sspi: new SSPI(options.bson),
'scram-sha-1': new ScramSHA1(options.bson)
};
this.authProviders = options.authProviders || defaultAuthProviders(options.bson);

// Contains the reconnect connection
this.reconnectConnection = null;
Expand Down Expand Up @@ -824,7 +812,7 @@ Pool.prototype.auth = function(mechanism) {

// Authenticate the connections
for (var i = 0; i < connections.length; i++) {
authenticate(self, args, connections[i], function(err) {
authenticate(self, args, connections[i], function(err, result) {
connectionsCount = connectionsCount - 1;

// Store the error
Expand All @@ -850,9 +838,9 @@ Pool.prototype.auth = function(mechanism) {
);
}

return cb(error);
return cb(error, result);
}
cb(null);
cb(null, result);
}
});
}
Expand All @@ -869,12 +857,12 @@ Pool.prototype.auth = function(mechanism) {
// Wait for loggout to finish
waitForLogout(self, function() {
// Authenticate all live connections
authenticateLiveConnections(self, args, function(err) {
authenticateLiveConnections(self, args, function(err, result) {
// Credentials correctly stored in auth provider if successful
// Any new connections will now reauthenticate correctly
self.authenticating = false;
// Return after authentication connections
callback(err);
callback(err, result);
});
});
};
Expand Down

0 comments on commit 506c087

Please sign in to comment.