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

Commit 506c087

Browse files
authored
feat(auth): adds saslprep and SCRAM-SHA-256
- 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
1 parent b0a3abb commit 506c087

File tree

10 files changed

+342
-104
lines changed

10 files changed

+342
-104
lines changed

index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@ module.exports = {
2828
// Raw operations
2929
Query: require('./lib/connection/commands').Query,
3030
// Auth mechanisms
31+
defaultAuthProviders: require('./lib/auth/defaultAuthProviders').defaultAuthProviders,
3132
MongoCR: require('./lib/auth/mongocr'),
3233
X509: require('./lib/auth/x509'),
3334
Plain: require('./lib/auth/plain'),
3435
GSSAPI: require('./lib/auth/gssapi'),
35-
ScramSHA1: require('./lib/auth/scram'),
36+
ScramSHA1: require('./lib/auth/scram').ScramSHA1,
37+
ScramSHA256: require('./lib/auth/scram').ScramSHA256,
3638
// Utilities
3739
parseConnectionString: require('./lib/uri_parser')
3840
};

lib/auth/defaultAuthProviders.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use strict';
2+
3+
const MongoCR = require('./mongocr');
4+
const X509 = require('./x509');
5+
const Plain = require('./plain');
6+
const GSSAPI = require('./gssapi');
7+
const SSPI = require('./sspi');
8+
const ScramSHA1 = require('./scram').ScramSHA1;
9+
const ScramSHA256 = require('./scram').ScramSHA256;
10+
11+
/**
12+
* Returns the default authentication providers.
13+
*
14+
* @param {BSON} bson Bson definition
15+
* @returns {Object} a mapping of auth names to auth types
16+
*/
17+
function defaultAuthProviders(bson) {
18+
return {
19+
mongocr: new MongoCR(bson),
20+
x509: new X509(bson),
21+
plain: new Plain(bson),
22+
gssapi: new GSSAPI(bson),
23+
sspi: new SSPI(bson),
24+
'scram-sha-1': new ScramSHA1(bson),
25+
'scram-sha-256': new ScramSHA256(bson)
26+
};
27+
}
28+
29+
module.exports = { defaultAuthProviders };

lib/auth/scram.js

Lines changed: 118 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ var f = require('util').format,
66
Query = require('../connection/commands').Query,
77
MongoError = require('../error').MongoError;
88

9+
let saslprep;
10+
11+
try {
12+
saslprep = require('saslprep');
13+
} catch (e) {
14+
// don't do anything;
15+
}
16+
917
var BSON = retrieveBSON(),
1018
Binary = BSON.Binary;
1119

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

2836
/**
29-
* Creates a new ScramSHA1 authentication mechanism
37+
* Creates a new ScramSHA authentication mechanism
3038
* @class
31-
* @return {ScramSHA1} A cursor instance
39+
* @return {ScramSHA} A cursor instance
3240
*/
33-
var ScramSHA1 = function(bson) {
41+
var ScramSHA = function(bson, cryptoMethod) {
3442
this.bson = bson;
3543
this.authStore = [];
3644
this.id = id++;
45+
this.cryptoMethod = cryptoMethod || 'sha1';
3746
};
3847

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

6271
// XOR two buffers
63-
var xor = function(a, b) {
72+
function xor(a, b) {
6473
if (!Buffer.isBuffer(a)) a = new Buffer(a);
6574
if (!Buffer.isBuffer(b)) b = new Buffer(b);
66-
var res = [];
67-
if (a.length > b.length) {
68-
for (var i = 0; i < b.length; i++) {
69-
res.push(a[i] ^ b[i]);
70-
}
71-
} else {
72-
for (i = 0; i < a.length; i++) {
73-
res.push(a[i] ^ b[i]);
74-
}
75+
const length = Math.max(a.length, b.length);
76+
const res = [];
77+
78+
for (let i = 0; i < length; i += 1) {
79+
res.push(a[i] ^ b[i]);
7580
}
76-
return new Buffer(res);
77-
};
81+
82+
return new Buffer(res).toString('base64');
83+
}
84+
85+
function H(method, text) {
86+
return crypto
87+
.createHash(method)
88+
.update(text)
89+
.digest();
90+
}
91+
92+
function HMAC(method, key, text) {
93+
return crypto
94+
.createHmac(method, key)
95+
.update(text)
96+
.digest();
97+
}
7898

7999
var _hiCache = {};
80100
var _hiCacheCount = 0;
@@ -83,15 +103,26 @@ var _hiCachePurge = function() {
83103
_hiCacheCount = 0;
84104
};
85105

86-
var hi = function(data, salt, iterations) {
106+
const hiLengthMap = {
107+
sha256: 32,
108+
sha1: 20
109+
};
110+
111+
function HI(data, salt, iterations, cryptoMethod) {
87112
// omit the work if already generated
88-
var key = [data, salt.toString('base64'), iterations].join('_');
113+
const key = [data, salt.toString('base64'), iterations].join('_');
89114
if (_hiCache[key] !== undefined) {
90115
return _hiCache[key];
91116
}
92117

93118
// generate the salt
94-
var saltedData = crypto.pbkdf2Sync(data, salt, iterations, 20, 'sha1');
119+
const saltedData = crypto.pbkdf2Sync(
120+
data,
121+
salt,
122+
iterations,
123+
hiLengthMap[cryptoMethod],
124+
cryptoMethod
125+
);
95126

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

106137
/**
107138
* Authenticate
@@ -114,7 +145,7 @@ var hi = function(data, salt, iterations) {
114145
* @param {authResultCallback} callback The callback to return the result from the authentication
115146
* @return {object}
116147
*/
117-
ScramSHA1.prototype.auth = function(server, connections, db, username, password, callback) {
148+
ScramSHA.prototype.auth = function(server, connections, db, username, password, callback) {
118149
var self = this;
119150
// Total connections
120151
var count = connections.length;
@@ -124,6 +155,25 @@ ScramSHA1.prototype.auth = function(server, connections, db, username, password,
124155
var numberOfValidConnections = 0;
125156
var errorObject = null;
126157

158+
const cryptoMethod = this.cryptoMethod;
159+
let mechanism = 'SCRAM-SHA-1';
160+
let processedPassword;
161+
162+
if (cryptoMethod === 'sha256') {
163+
mechanism = 'SCRAM-SHA-256';
164+
165+
let saslprepFn = (server.s && server.s.saslprep) || saslprep;
166+
167+
if (saslprepFn) {
168+
processedPassword = saslprepFn(password);
169+
} else {
170+
console.warn('Warning: no saslprep library specified. Passwords will not be sanitized');
171+
processedPassword = password;
172+
}
173+
} else {
174+
processedPassword = passwordDigest(username, password);
175+
}
176+
127177
// Execute MongoCR
128178
var executeScram = function(connection) {
129179
// Clean up the user
@@ -132,13 +182,21 @@ ScramSHA1.prototype.auth = function(server, connections, db, username, password,
132182
// Create a random nonce
133183
var nonce = crypto.randomBytes(24).toString('base64');
134184
// var nonce = 'MsQUY9iw0T9fx2MUEz6LZPwGuhVvWAhc'
135-
var firstBare = f('n=%s,r=%s', username, nonce);
185+
186+
// NOTE: This is done b/c Javascript uses UTF-16, but the server is hashing in UTF-8.
187+
// Since the username is not sasl-prep-d, we need to do this here.
188+
const firstBare = Buffer.concat([
189+
Buffer.from('n=', 'utf8'),
190+
Buffer.from(username, 'utf8'),
191+
Buffer.from(',r=', 'utf8'),
192+
Buffer.from(nonce, 'utf8')
193+
]);
136194

137195
// Build command structure
138196
var cmd = {
139197
saslStart: 1,
140-
mechanism: 'SCRAM-SHA-1',
141-
payload: new Binary(f('n,,%s', firstBare)),
198+
mechanism: mechanism,
199+
payload: new Binary(Buffer.concat([Buffer.from('n,,', 'utf8'), firstBare])),
142200
autoAuthorize: 1
143201
};
144202

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

221279
// Set up start of proof
222280
var withoutProof = f('c=biws,r=%s', rnonce);
223-
var passwordDig = passwordDigest(username, password);
224-
var saltedPassword = hi(passwordDig, new Buffer(salt, 'base64'), iterations);
281+
var saltedPassword = HI(
282+
processedPassword,
283+
new Buffer(salt, 'base64'),
284+
iterations,
285+
cryptoMethod
286+
);
287+
288+
if (iterations && iterations < 4096) {
289+
const error = new MongoError(`Server returned an invalid iteration count ${iterations}`);
290+
return callback(error, false);
291+
}
225292

226293
// Create the client key
227-
var hmac = crypto.createHmac('sha1', saltedPassword);
228-
hmac.update(new Buffer('Client Key'));
229-
var clientKey = new Buffer(hmac.digest('base64'), 'base64');
294+
const clientKey = HMAC(cryptoMethod, saltedPassword, 'Client Key');
230295

231296
// Create the stored key
232-
var hash = crypto.createHash('sha1');
233-
hash.update(clientKey);
234-
var storedKey = new Buffer(hash.digest('base64'), 'base64');
297+
const storedKey = H(cryptoMethod, clientKey);
235298

236299
// Create the authentication message
237-
var authMsg = [firstBare, r.result.payload.value().toString('base64'), withoutProof].join(
238-
','
239-
);
300+
const authMessage = [
301+
firstBare,
302+
r.result.payload.value().toString('base64'),
303+
withoutProof
304+
].join(',');
240305

241306
// Create client signature
242-
hmac = crypto.createHmac('sha1', storedKey);
243-
hmac.update(new Buffer(authMsg));
244-
var clientSig = new Buffer(hmac.digest('base64'), 'base64');
307+
const clientSignature = HMAC(cryptoMethod, storedKey, authMessage);
245308

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

249312
// Create client final
250-
var clientFinal = [withoutProof, clientProof].join(',');
313+
const clientFinal = [withoutProof, clientProof].join(',');
251314

252-
//
253315
// Create continue message
254-
var cmd = {
316+
const cmd = {
255317
saslContinue: 1,
256318
conversationId: r.result.conversationId,
257319
payload: new Binary(new Buffer(clientFinal))
@@ -326,7 +388,7 @@ var addAuthSession = function(authStore, session) {
326388
* @param {string} db Name of database we are removing authStore details about
327389
* @return {object}
328390
*/
329-
ScramSHA1.prototype.logout = function(dbName) {
391+
ScramSHA.prototype.logout = function(dbName) {
330392
this.authStore = this.authStore.filter(function(x) {
331393
return x.db !== dbName;
332394
});
@@ -340,7 +402,7 @@ ScramSHA1.prototype.logout = function(dbName) {
340402
* @param {authResultCallback} callback The callback to return the result from the authentication
341403
* @return {object}
342404
*/
343-
ScramSHA1.prototype.reauthenticate = function(server, connections, callback) {
405+
ScramSHA.prototype.reauthenticate = function(server, connections, callback) {
344406
var authStore = this.authStore.slice(0);
345407
var count = authStore.length;
346408
// No connections
@@ -364,4 +426,16 @@ ScramSHA1.prototype.reauthenticate = function(server, connections, callback) {
364426
}
365427
};
366428

367-
module.exports = ScramSHA1;
429+
class ScramSHA1 extends ScramSHA {
430+
constructor(bson) {
431+
super(bson, 'sha1');
432+
}
433+
}
434+
435+
class ScramSHA256 extends ScramSHA {
436+
constructor(bson) {
437+
super(bson, 'sha256');
438+
}
439+
}
440+
441+
module.exports = { ScramSHA1, ScramSHA256 };

lib/connection/pool.js

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,7 @@ var inherits = require('util').inherits,
1818

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

21-
var MongoCR = require('../auth/mongocr'),
22-
X509 = require('../auth/x509'),
23-
Plain = require('../auth/plain'),
24-
GSSAPI = require('../auth/gssapi'),
25-
SSPI = require('../auth/sspi'),
26-
ScramSHA1 = require('../auth/scram');
21+
const defaultAuthProviders = require('../auth/defaultAuthProviders').defaultAuthProviders;
2722

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

149144
// All the authProviders
150-
this.authProviders = options.authProviders || {
151-
mongocr: new MongoCR(options.bson),
152-
x509: new X509(options.bson),
153-
plain: new Plain(options.bson),
154-
gssapi: new GSSAPI(options.bson),
155-
sspi: new SSPI(options.bson),
156-
'scram-sha-1': new ScramSHA1(options.bson)
157-
};
145+
this.authProviders = options.authProviders || defaultAuthProviders(options.bson);
158146

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

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

830818
// Store the error
@@ -850,9 +838,9 @@ Pool.prototype.auth = function(mechanism) {
850838
);
851839
}
852840

853-
return cb(error);
841+
return cb(error, result);
854842
}
855-
cb(null);
843+
cb(null, result);
856844
}
857845
});
858846
}
@@ -869,12 +857,12 @@ Pool.prototype.auth = function(mechanism) {
869857
// Wait for loggout to finish
870858
waitForLogout(self, function() {
871859
// Authenticate all live connections
872-
authenticateLiveConnections(self, args, function(err) {
860+
authenticateLiveConnections(self, args, function(err, result) {
873861
// Credentials correctly stored in auth provider if successful
874862
// Any new connections will now reauthenticate correctly
875863
self.authenticating = false;
876864
// Return after authentication connections
877-
callback(err);
865+
callback(err, result);
878866
});
879867
});
880868
};

0 commit comments

Comments
 (0)