Skip to content

Commit dcea87a

Browse files
committed
Support authentication switch request. Fixes #1396
Add optional auth plugin name and data to UseOldPasswordPacket. Support mysql_native_password and mysql_old_password as potential authentication methods (but still require 'insecureAuth:true' for the latter).
1 parent 5c60778 commit dcea87a

File tree

7 files changed

+114
-11
lines changed

7 files changed

+114
-11
lines changed

lib/protocol/Parser.js

+5
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,11 @@ Parser.prototype.parseLengthCodedBuffer = function() {
217217
return this.parseBuffer(length);
218218
};
219219

220+
Parser.prototype.parsePacketTerminatedBuffer = function() {
221+
var length = this._packetEnd - this._offset;
222+
return this.parseBuffer(length);
223+
};
224+
220225
Parser.prototype.parseLengthCodedNumber = function parseLengthCodedNumber() {
221226
if (this._offset >= this._buffer.length) {
222227
var err = new Error('Parser: read past end');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module.exports = AuthenticationSwitchResponsePacket;
2+
function AuthenticationSwitchResponsePacket(options) {
3+
options = options || {};
4+
5+
this.scrambleBuff = options.scrambleBuff;
6+
}
7+
8+
AuthenticationSwitchResponsePacket.prototype.parse = function(parser) {
9+
this.scrambleBuff = parser.parsePacketTerminatedBuffer();
10+
};
11+
12+
AuthenticationSwitchResponsePacket.prototype.write = function(writer) {
13+
writer.writeBuffer(this.scrambleBuff);
14+
};

lib/protocol/packets/UseOldPasswordPacket.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,25 @@ module.exports = UseOldPasswordPacket;
22
function UseOldPasswordPacket(options) {
33
options = options || {};
44

5-
this.firstByte = options.firstByte || 0xfe;
5+
this.firstByte = options.firstByte || 0xfe;
6+
this.methodName = options.methodName;
7+
this.pluginData = options.pluginData;
68
}
79

810
UseOldPasswordPacket.prototype.parse = function(parser) {
911
this.firstByte = parser.parseUnsignedNumber(1);
12+
if (!parser.reachedPacketEnd()) {
13+
this.methodName = parser.parseNullTerminatedString();
14+
this.pluginData = parser.parsePacketTerminatedBuffer();
15+
}
1016
};
1117

1218
UseOldPasswordPacket.prototype.write = function(writer) {
1319
writer.writeUnsignedNumber(1, this.firstByte);
20+
if (this.methodName !== undefined) {
21+
writer.writeNullTerminatedString(this.methodName);
22+
if (this.pluginData !== undefined) {
23+
writer.writeBuffer(this.pluginData);
24+
}
25+
}
1426
};

lib/protocol/packets/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
exports.AuthenticationSwitchResponsePacket = require('./AuthenticationSwitchResponsePacket');
12
exports.ClientAuthenticationPacket = require('./ClientAuthenticationPacket');
23
exports.ComChangeUserPacket = require('./ComChangeUserPacket');
34
exports.ComPingPacket = require('./ComPingPacket');

lib/protocol/sequences/Handshake.js

+33-10
Original file line numberDiff line numberDiff line change
@@ -80,23 +80,46 @@ Handshake.prototype._sendCredentials = function() {
8080
}));
8181
};
8282

83-
Handshake.prototype['UseOldPasswordPacket'] = function() {
84-
if (!this._config.insecureAuth) {
83+
Handshake.prototype['UseOldPasswordPacket'] = function(packet) {
84+
if (!packet || !packet.methodName || packet.methodName === 'mysql_old_password') {
85+
if (!this._config.insecureAuth) {
86+
var err = new Error(
87+
'MySQL server is requesting the old and insecure pre-4.1 auth mechanism.' +
88+
'Upgrade the user password or use the {insecureAuth: true} option.'
89+
);
90+
91+
err.code = 'HANDSHAKE_INSECURE_AUTH';
92+
err.fatal = true;
93+
94+
this.end(err);
95+
return;
96+
}
97+
98+
this.emit('packet', new Packets.OldPasswordPacket({
99+
scrambleBuff: Auth.scramble323(packet.pluginData || this._handshakeInitializationPacket.scrambleBuff(), this._config.password)
100+
}));
101+
} else if (packet.methodName === 'mysql_native_password') {
102+
// "auth plugin data" is documented as "string[EOF]", but MySQL Server will send a
103+
// null-terminated byte array for mysql_native_password; we only need to hash with
104+
// the first 20 bytes
105+
var challenge = packet.pluginData;
106+
if (challenge.length === 21 && challenge[20] === 0) {
107+
challenge = challenge.slice(0, 20);
108+
}
109+
110+
this.emit('packet', new Packets.AuthenticationSwitchResponsePacket({
111+
scrambleBuff: Auth.token(this._config.password, challenge)
112+
}));
113+
} else {
85114
var err = new Error(
86-
'MySQL server is requesting the old and insecure pre-4.1 auth mechanism.' +
87-
'Upgrade the user password or use the {insecureAuth: true} option.'
115+
'MySQL is requesting the ' + packet.methodName + ' authentication method, which is not supported.'
88116
);
89117

90-
err.code = 'HANDSHAKE_INSECURE_AUTH';
118+
err.code = 'UNSUPPORTED_AUTH_METHOD';
91119
err.fatal = true;
92120

93121
this.end(err);
94-
return;
95122
}
96-
97-
this.emit('packet', new Packets.OldPasswordPacket({
98-
scrambleBuff: Auth.scramble323(this._handshakeInitializationPacket.scrambleBuff(), this._config.password)
99-
}));
100123
};
101124

102125
Handshake.prototype['ErrorPacket'] = function(packet) {

test/FakeServer.js

+14
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ function FakeConnection(socket) {
6464
this._handshakeInitializationPacket = null;
6565
this._clientAuthenticationPacket = null;
6666
this._oldPasswordPacket = null;
67+
this._authSwitchResponse = null;
6768
this._handshakeOptions = {};
6869

6970
socket.on('data', this._handleData.bind(this));
@@ -269,6 +270,8 @@ FakeConnection.prototype._parsePacket = function(header) {
269270
this._clientAuthenticationPacket = packet;
270271
if (this._handshakeOptions.oldPassword) {
271272
this._sendPacket(new Packets.UseOldPasswordPacket());
273+
} else if (this._handshakeOptions.forceAuthSwitch) {
274+
this._sendPacket(new Packets.UseOldPasswordPacket({ methodName: 'mysql_native_password', pluginData: Buffer.from('00112233445566778899AABBCCDDEEFF0102030400', 'hex') }));
272275
} else if (this._handshakeOptions.password === 'passwd') {
273276
var expected = Buffer.from('3DA0ADA7C9E1BB3A110575DF53306F9D2DE7FD09', 'hex');
274277
this._sendAuthResponse(packet, expected);
@@ -287,6 +290,13 @@ FakeConnection.prototype._parsePacket = function(header) {
287290

288291
var expected = Auth.scramble323(this._handshakeInitializationPacket.scrambleBuff(), this._handshakeOptions.password);
289292

293+
this._sendAuthResponse(packet, expected);
294+
break;
295+
case Packets.AuthenticationSwitchResponsePacket:
296+
this._authSwitchResponse = packet;
297+
298+
var expected = Auth.token(this._handshakeOptions.password, Buffer.from('00112233445566778899AABBCCDDEEFF01020304', 'hex'));
299+
290300
this._sendAuthResponse(packet, expected);
291301
break;
292302
case Packets.ComQueryPacket:
@@ -355,6 +365,10 @@ FakeConnection.prototype._determinePacket = function(header) {
355365
return Packets.OldPasswordPacket;
356366
}
357367

368+
if (this._handshakeOptions.forceAuthSwitch && !this._authSwitchResponse) {
369+
return Packets.AuthenticationSwitchResponsePacket;
370+
}
371+
358372
var firstByte = this._parser.peak();
359373
switch (firstByte) {
360374
case 0x01: return Packets.ComQuitPacket;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
var common = require('../../common');
2+
var connection = common.createConnection({
3+
port : common.fakeServerPort,
4+
password : 'authswitch'
5+
});
6+
var assert = require('assert');
7+
8+
var server = common.createFakeServer();
9+
10+
var connected;
11+
server.listen(common.fakeServerPort, function(err) {
12+
if (err) throw err;
13+
14+
connection.connect(function(err, result) {
15+
if (err) throw err;
16+
17+
connected = result;
18+
19+
connection.destroy();
20+
server.destroy();
21+
});
22+
});
23+
24+
server.on('connection', function(incomingConnection) {
25+
incomingConnection.handshake({
26+
user : connection.config.user,
27+
password : connection.config.password,
28+
forceAuthSwitch : true
29+
});
30+
});
31+
32+
process.on('exit', function() {
33+
assert.equal(connected.fieldCount, 0);
34+
});

0 commit comments

Comments
 (0)