@@ -6,6 +6,14 @@ var f = require('util').format,
6
6
Query = require ( '../connection/commands' ) . Query ,
7
7
MongoError = require ( '../error' ) . MongoError ;
8
8
9
+ let saslprep ;
10
+
11
+ try {
12
+ saslprep = require ( 'saslprep' ) ;
13
+ } catch ( e ) {
14
+ // don't do anything;
15
+ }
16
+
9
17
var BSON = retrieveBSON ( ) ,
10
18
Binary = BSON . Binary ;
11
19
@@ -26,14 +34,15 @@ AuthSession.prototype.equal = function(session) {
26
34
var id = 0 ;
27
35
28
36
/**
29
- * Creates a new ScramSHA1 authentication mechanism
37
+ * Creates a new ScramSHA authentication mechanism
30
38
* @class
31
- * @return {ScramSHA1 } A cursor instance
39
+ * @return {ScramSHA } A cursor instance
32
40
*/
33
- var ScramSHA1 = function ( bson ) {
41
+ var ScramSHA = function ( bson , cryptoMethod ) {
34
42
this . bson = bson ;
35
43
this . authStore = [ ] ;
36
44
this . id = id ++ ;
45
+ this . cryptoMethod = cryptoMethod || 'sha1' ;
37
46
} ;
38
47
39
48
var parsePayload = function ( payload ) {
@@ -60,21 +69,32 @@ var passwordDigest = function(username, password) {
60
69
} ;
61
70
62
71
// XOR two buffers
63
- var xor = function ( a , b ) {
72
+ function xor ( a , b ) {
64
73
if ( ! Buffer . isBuffer ( a ) ) a = new Buffer ( a ) ;
65
74
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 ] ) ;
75
80
}
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
+ }
78
98
79
99
var _hiCache = { } ;
80
100
var _hiCacheCount = 0 ;
@@ -83,15 +103,26 @@ var _hiCachePurge = function() {
83
103
_hiCacheCount = 0 ;
84
104
} ;
85
105
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 ) {
87
112
// omit the work if already generated
88
- var key = [ data , salt . toString ( 'base64' ) , iterations ] . join ( '_' ) ;
113
+ const key = [ data , salt . toString ( 'base64' ) , iterations ] . join ( '_' ) ;
89
114
if ( _hiCache [ key ] !== undefined ) {
90
115
return _hiCache [ key ] ;
91
116
}
92
117
93
118
// 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
+ ) ;
95
126
96
127
// cache a copy to speed up the next lookup, but prevent unbounded cache growth
97
128
if ( _hiCacheCount >= 200 ) {
@@ -101,7 +132,7 @@ var hi = function(data, salt, iterations) {
101
132
_hiCache [ key ] = saltedData ;
102
133
_hiCacheCount += 1 ;
103
134
return saltedData ;
104
- } ;
135
+ }
105
136
106
137
/**
107
138
* Authenticate
@@ -114,7 +145,7 @@ var hi = function(data, salt, iterations) {
114
145
* @param {authResultCallback } callback The callback to return the result from the authentication
115
146
* @return {object }
116
147
*/
117
- ScramSHA1 . prototype . auth = function ( server , connections , db , username , password , callback ) {
148
+ ScramSHA . prototype . auth = function ( server , connections , db , username , password , callback ) {
118
149
var self = this ;
119
150
// Total connections
120
151
var count = connections . length ;
@@ -124,6 +155,25 @@ ScramSHA1.prototype.auth = function(server, connections, db, username, password,
124
155
var numberOfValidConnections = 0 ;
125
156
var errorObject = null ;
126
157
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
+
127
177
// Execute MongoCR
128
178
var executeScram = function ( connection ) {
129
179
// Clean up the user
@@ -132,13 +182,21 @@ ScramSHA1.prototype.auth = function(server, connections, db, username, password,
132
182
// Create a random nonce
133
183
var nonce = crypto . randomBytes ( 24 ) . toString ( 'base64' ) ;
134
184
// 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
+ ] ) ;
136
194
137
195
// Build command structure
138
196
var cmd = {
139
197
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 ] ) ) ,
142
200
autoAuthorize : 1
143
201
} ;
144
202
@@ -220,38 +278,42 @@ ScramSHA1.prototype.auth = function(server, connections, db, username, password,
220
278
221
279
// Set up start of proof
222
280
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
+ }
225
292
226
293
// 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' ) ;
230
295
231
296
// 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 ) ;
235
298
236
299
// 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 ( ',' ) ;
240
305
241
306
// 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 ) ;
245
308
246
309
// 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 ) ) ;
248
311
249
312
// Create client final
250
- var clientFinal = [ withoutProof , clientProof ] . join ( ',' ) ;
313
+ const clientFinal = [ withoutProof , clientProof ] . join ( ',' ) ;
251
314
252
- //
253
315
// Create continue message
254
- var cmd = {
316
+ const cmd = {
255
317
saslContinue : 1 ,
256
318
conversationId : r . result . conversationId ,
257
319
payload : new Binary ( new Buffer ( clientFinal ) )
@@ -326,7 +388,7 @@ var addAuthSession = function(authStore, session) {
326
388
* @param {string } db Name of database we are removing authStore details about
327
389
* @return {object }
328
390
*/
329
- ScramSHA1 . prototype . logout = function ( dbName ) {
391
+ ScramSHA . prototype . logout = function ( dbName ) {
330
392
this . authStore = this . authStore . filter ( function ( x ) {
331
393
return x . db !== dbName ;
332
394
} ) ;
@@ -340,7 +402,7 @@ ScramSHA1.prototype.logout = function(dbName) {
340
402
* @param {authResultCallback } callback The callback to return the result from the authentication
341
403
* @return {object }
342
404
*/
343
- ScramSHA1 . prototype . reauthenticate = function ( server , connections , callback ) {
405
+ ScramSHA . prototype . reauthenticate = function ( server , connections , callback ) {
344
406
var authStore = this . authStore . slice ( 0 ) ;
345
407
var count = authStore . length ;
346
408
// No connections
@@ -364,4 +426,16 @@ ScramSHA1.prototype.reauthenticate = function(server, connections, callback) {
364
426
}
365
427
} ;
366
428
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 } ;
0 commit comments