Skip to content

Commit 806cd62

Browse files
committed
fix(scram): verify server digest, ensuring mutual authentication
NODE-2376
1 parent cb107a8 commit 806cd62

File tree

2 files changed

+89
-6
lines changed

2 files changed

+89
-6
lines changed

lib/core/auth/scram.js

+42-6
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ try {
1919
var parsePayload = function(payload) {
2020
var dict = {};
2121
var parts = payload.split(',');
22-
2322
for (var i = 0; i < parts.length; i++) {
2423
var valueParts = parts[i].split('=');
2524
dict[valueParts[0]] = valueParts[1];
@@ -105,6 +104,23 @@ function HI(data, salt, iterations, cryptoMethod) {
105104
return saltedData;
106105
}
107106

107+
function compareDigest(lhs, rhs) {
108+
if (lhs.length !== rhs.length) {
109+
return false;
110+
}
111+
112+
if (typeof crypto.timingSafeEqual === 'function') {
113+
return crypto.timingSafeEqual(lhs, rhs);
114+
}
115+
116+
let result = 0;
117+
for (let i = 0; i < lhs.length; i++) {
118+
result |= lhs[i] ^ rhs[i];
119+
}
120+
121+
return result === 0;
122+
}
123+
108124
/**
109125
* Creates a new ScramSHA authentication mechanism
110126
* @class
@@ -179,9 +195,19 @@ class ScramSHA extends AuthProvider {
179195

180196
const payload = Buffer.isBuffer(r.payload) ? new Binary(r.payload) : r.payload;
181197
const dict = parsePayload(payload.value());
198+
182199
const iterations = parseInt(dict.i, 10);
200+
if (iterations && iterations < 4096) {
201+
callback(new MongoError(`Server returned an invalid iteration count ${iterations}`), false);
202+
return;
203+
}
204+
183205
const salt = dict.s;
184206
const rnonce = dict.r;
207+
if (rnonce.startsWith('nonce')) {
208+
callback(new MongoError(`Server returned an invalid nonce: ${rnonce}`), false);
209+
return;
210+
}
185211

186212
// Set up start of proof
187213
const withoutProof = `c=biws,r=${rnonce}`;
@@ -192,25 +218,35 @@ class ScramSHA extends AuthProvider {
192218
cryptoMethod
193219
);
194220

195-
if (iterations && iterations < 4096) {
196-
const error = new MongoError(`Server returned an invalid iteration count ${iterations}`);
197-
return callback(error, false);
198-
}
199-
200221
const clientKey = HMAC(cryptoMethod, saltedPassword, 'Client Key');
222+
const serverKey = HMAC(cryptoMethod, saltedPassword, 'Server Key');
201223
const storedKey = H(cryptoMethod, clientKey);
202224
const authMessage = [firstBare, payload.value().toString('base64'), withoutProof].join(',');
203225

204226
const clientSignature = HMAC(cryptoMethod, storedKey, authMessage);
205227
const clientProof = `p=${xor(clientKey, clientSignature)}`;
206228
const clientFinal = [withoutProof, clientProof].join(',');
229+
230+
const serverSignature = HMAC(cryptoMethod, serverKey, authMessage);
231+
207232
const saslContinueCmd = {
208233
saslContinue: 1,
209234
conversationId: r.conversationId,
210235
payload: new Binary(Buffer.from(clientFinal))
211236
};
212237

213238
sendAuthCommand(connection, `${db}.$cmd`, saslContinueCmd, (err, r) => {
239+
if (r && typeof r.ok === 'number' && r.ok === 0) {
240+
callback(err, r);
241+
return;
242+
}
243+
244+
const parsedResponse = parsePayload(r.payload.value());
245+
if (!compareDigest(Buffer.from(parsedResponse.v, 'base64'), serverSignature)) {
246+
callback(new MongoError('Server returned an invalid signature'));
247+
return;
248+
}
249+
214250
if (!r || r.done !== false) {
215251
return callback(err, r);
216252
}

test/unit/core/scram_iterations.test.js

+47
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,51 @@ describe('SCRAM Iterations Tests', function() {
6565

6666
client.connect();
6767
});
68+
69+
it('should error if server digest is invalid', function(_done) {
70+
const credentials = new MongoCredentials({
71+
mechanism: 'default',
72+
source: 'db',
73+
username: 'user',
74+
password: 'pencil'
75+
});
76+
77+
let done = e => {
78+
done = () => {};
79+
return _done(e);
80+
};
81+
82+
test.server.setMessageHandler(request => {
83+
const doc = request.document;
84+
if (doc.ismaster) {
85+
return request.reply(Object.assign({}, mock.DEFAULT_ISMASTER));
86+
} else if (doc.saslStart) {
87+
return request.reply({
88+
ok: 1,
89+
done: false,
90+
payload: Buffer.from(
91+
'r=VNnXkRqKflB5+rmfnFiisCWzgDLzez02iRpbvE5mQjMvizb+VkSPRZZ/pDmFzLxq,s=dZTyOb+KZqoeTFdsULiqow==,i=10000'
92+
)
93+
});
94+
} else if (doc.saslContinue) {
95+
return request.reply({
96+
ok: 1,
97+
done: false,
98+
payload: Buffer.from('v=bWFsaWNpb3VzbWFsaWNpb3VzVzV')
99+
});
100+
}
101+
});
102+
103+
const client = new Server(Object.assign({}, test.server.address(), { credentials }));
104+
client.on('error', err => {
105+
expect(err).to.not.be.null;
106+
expect(err)
107+
.to.have.property('message')
108+
.that.matches(/Server returned an invalid signature/);
109+
110+
client.destroy(done);
111+
});
112+
113+
client.connect();
114+
});
68115
});

0 commit comments

Comments
 (0)