Skip to content

Commit

Permalink
Support authentication switch request. Fixes mysqljs#1396
Browse files Browse the repository at this point in the history
Rename UseOldPasswordPacket to AuthenticationMethodSwitchRequestPacket
and implement its two other fields: plugin name & data.

Support mysql_native_password and mysql_old_password as potential
authentication methods (but still require 'insecureAuth:true' for the
latter).
  • Loading branch information
bgrainger committed May 14, 2017
1 parent 5c60778 commit 2ae97d5
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 27 deletions.
5 changes: 5 additions & 0 deletions lib/protocol/Parser.js
Expand Up @@ -217,6 +217,11 @@ Parser.prototype.parseLengthCodedBuffer = function() {
return this.parseBuffer(length);
};

Parser.prototype.parsePacketTerminatedBuffer = function() {
var length = this._packetEnd - this._offset;
return this.parseBuffer(length);
};

Parser.prototype.parseLengthCodedNumber = function parseLengthCodedNumber() {
if (this._offset >= this._buffer.length) {
var err = new Error('Parser: read past end');
Expand Down
22 changes: 22 additions & 0 deletions lib/protocol/packets/AuthenticationMethodSwitchRequestPacket.js
@@ -0,0 +1,22 @@
module.exports = AuthenticationMethodSwitchRequestPacket;
function AuthenticationMethodSwitchRequestPacket(options) {
options = options || {};

this.command = 0xfe;
this.methodName = options.methodName || 'mysql_native_password';
this.pluginData = options.pluginData;
}

AuthenticationMethodSwitchRequestPacket.prototype.parse = function(parser) {
this.command = parser.parseUnsignedNumber(1);
this.methodName = parser.parseNullTerminatedString();
this.pluginData = parser.parsePacketTerminatedBuffer();
};

AuthenticationMethodSwitchRequestPacket.prototype.write = function(writer) {
writer.writeUnsignedNumber(1, this.command);
writer.writeNullTerminatedString(this.methodName);
if (this.pluginData !== undefined) {
writer.writeBuffer(this.pluginData);
}
};
14 changes: 14 additions & 0 deletions lib/protocol/packets/AuthenticationSwitchResponsePacket.js
@@ -0,0 +1,14 @@
module.exports = AuthenticationSwitchResponsePacket;
function AuthenticationSwitchResponsePacket(options) {
options = options || {};

this.scrambleBuff = options.scrambleBuff;
}

AuthenticationSwitchResponsePacket.prototype.parse = function(parser) {
this.scrambleBuff = parser.parsePacketTerminatedBuffer();
};

AuthenticationSwitchResponsePacket.prototype.write = function(writer) {
writer.writeBuffer(this.scrambleBuff);
};
14 changes: 0 additions & 14 deletions lib/protocol/packets/UseOldPasswordPacket.js

This file was deleted.

3 changes: 2 additions & 1 deletion lib/protocol/packets/index.js
@@ -1,3 +1,5 @@
exports.AuthenticationMethodSwitchRequestPacket = require('./AuthenticationMethodSwitchRequestPacket');
exports.AuthenticationSwitchResponsePacket = require('./AuthenticationSwitchResponsePacket');
exports.ClientAuthenticationPacket = require('./ClientAuthenticationPacket');
exports.ComChangeUserPacket = require('./ComChangeUserPacket');
exports.ComPingPacket = require('./ComPingPacket');
Expand All @@ -17,4 +19,3 @@ exports.ResultSetHeaderPacket = require('./ResultSetHeaderPacket');
exports.RowDataPacket = require('./RowDataPacket');
exports.SSLRequestPacket = require('./SSLRequestPacket');
exports.StatisticsPacket = require('./StatisticsPacket');
exports.UseOldPasswordPacket = require('./UseOldPasswordPacket');
45 changes: 34 additions & 11 deletions lib/protocol/sequences/Handshake.js
Expand Up @@ -25,7 +25,7 @@ Handshake.prototype.determinePacket = function(firstByte) {
}

if (firstByte === 0xfe) {
return Packets.UseOldPasswordPacket;
return Packets.AuthenticationMethodSwitchRequestPacket;
}

return undefined;
Expand Down Expand Up @@ -80,23 +80,46 @@ Handshake.prototype._sendCredentials = function() {
}));
};

Handshake.prototype['UseOldPasswordPacket'] = function() {
if (!this._config.insecureAuth) {
Handshake.prototype['AuthenticationMethodSwitchRequestPacket'] = function(packet) {
if (packet.methodName === 'mysql_native_password') {
// "auth plugin data" is documented as "string[EOF]", but MySQL Server will send a
// null-terminated byte array for mysql_native_password; we only need to hash with
// the first 20 bytes
var challenge = packet.pluginData;
if (challenge.length === 21) {
challenge = challenge.slice(0, 20);
}

this.emit('packet', new Packets.AuthenticationSwitchResponsePacket({
scrambleBuff: Auth.token(this._config.password, challenge)
}));
} else if (packet.methodName === 'mysql_old_password') {
if (!this._config.insecureAuth) {
var err = new Error(
'MySQL server is requesting the old and insecure pre-4.1 auth mechanism.' +
'Upgrade the user password or use the {insecureAuth: true} option.'
);

err.code = 'HANDSHAKE_INSECURE_AUTH';
err.fatal = true;

this.end(err);
return;
}

this.emit('packet', new Packets.OldPasswordPacket({
scrambleBuff: Auth.scramble323(this._handshakeInitializationPacket.scrambleBuff(), this._config.password)
}));
} else {
var err = new Error(
'MySQL server is requesting the old and insecure pre-4.1 auth mechanism.' +
'Upgrade the user password or use the {insecureAuth: true} option.'
'MySQL is requesting the ' + packet.methodName + ' authentication method, which is not supported.'
);

err.code = 'HANDSHAKE_INSECURE_AUTH';
err.code = 'UNSUPPORTED_AUTH_METHOD';
err.fatal = true;

this.end(err);
return;
}

this.emit('packet', new Packets.OldPasswordPacket({
scrambleBuff: Auth.scramble323(this._handshakeInitializationPacket.scrambleBuff(), this._config.password)
}));
};

Handshake.prototype['ErrorPacket'] = function(packet) {
Expand Down
16 changes: 15 additions & 1 deletion test/FakeServer.js
Expand Up @@ -64,6 +64,7 @@ function FakeConnection(socket) {
this._handshakeInitializationPacket = null;
this._clientAuthenticationPacket = null;
this._oldPasswordPacket = null;
this._authSwitchResponse = null;
this._handshakeOptions = {};

socket.on('data', this._handleData.bind(this));
Expand Down Expand Up @@ -268,7 +269,9 @@ FakeConnection.prototype._parsePacket = function(header) {
case Packets.ClientAuthenticationPacket:
this._clientAuthenticationPacket = packet;
if (this._handshakeOptions.oldPassword) {
this._sendPacket(new Packets.UseOldPasswordPacket());
this._sendPacket(new Packets.AuthenticationMethodSwitchRequestPacket({ methodName: 'mysql_old_password' }));
} else if (this._handshakeOptions.forceAuthSwitch) {
this._sendPacket(new Packets.AuthenticationMethodSwitchRequestPacket({ pluginData: Buffer.from('00112233445566778899AABBCCDDEEFF0102030400', 'hex') }));
} else if (this._handshakeOptions.password === 'passwd') {
var expected = Buffer.from('3DA0ADA7C9E1BB3A110575DF53306F9D2DE7FD09', 'hex');
this._sendAuthResponse(packet, expected);
Expand All @@ -287,6 +290,13 @@ FakeConnection.prototype._parsePacket = function(header) {

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

this._sendAuthResponse(packet, expected);
break;
case Packets.AuthenticationSwitchResponsePacket:
this._authSwitchResponse = packet;

var expected = Auth.token(this._handshakeOptions.password, Buffer.from('00112233445566778899AABBCCDDEEFF01020304', 'hex'));

this._sendAuthResponse(packet, expected);
break;
case Packets.ComQueryPacket:
Expand Down Expand Up @@ -355,6 +365,10 @@ FakeConnection.prototype._determinePacket = function(header) {
return Packets.OldPasswordPacket;
}

if (this._handshakeOptions.forceAuthSwitch && !this._authSwitchResponse) {
return Packets.AuthenticationSwitchResponsePacket;
}

var firstByte = this._parser.peak();
switch (firstByte) {
case 0x01: return Packets.ComQuitPacket;
Expand Down
34 changes: 34 additions & 0 deletions test/unit/connection/test-force-auth-switch.js
@@ -0,0 +1,34 @@
var common = require('../../common');
var connection = common.createConnection({
port : common.fakeServerPort,
password : 'authswitch'
});
var assert = require('assert');

var server = common.createFakeServer();

var connected;
server.listen(common.fakeServerPort, function(err) {
if (err) throw err;

connection.connect(function(err, result) {
if (err) throw err;

connected = result;

connection.destroy();
server.destroy();
});
});

server.on('connection', function(incomingConnection) {
incomingConnection.handshake({
user : connection.config.user,
password : connection.config.password,
forceAuthSwitch : true
});
});

process.on('exit', function() {
assert.equal(connected.fieldCount, 0);
});

0 comments on commit 2ae97d5

Please sign in to comment.