diff --git a/lib/protocol/Parser.js b/lib/protocol/Parser.js index c61b3e9e5..e1149a58b 100644 --- a/lib/protocol/Parser.js +++ b/lib/protocol/Parser.js @@ -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'); diff --git a/lib/protocol/packets/AuthenticationMethodSwitchRequestPacket.js b/lib/protocol/packets/AuthenticationMethodSwitchRequestPacket.js new file mode 100644 index 000000000..18af2efae --- /dev/null +++ b/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); + } +}; diff --git a/lib/protocol/packets/AuthenticationSwitchResponsePacket.js b/lib/protocol/packets/AuthenticationSwitchResponsePacket.js new file mode 100644 index 000000000..21f3881d0 --- /dev/null +++ b/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); +}; diff --git a/lib/protocol/packets/UseOldPasswordPacket.js b/lib/protocol/packets/UseOldPasswordPacket.js deleted file mode 100644 index d73bf4459..000000000 --- a/lib/protocol/packets/UseOldPasswordPacket.js +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = UseOldPasswordPacket; -function UseOldPasswordPacket(options) { - options = options || {}; - - this.firstByte = options.firstByte || 0xfe; -} - -UseOldPasswordPacket.prototype.parse = function(parser) { - this.firstByte = parser.parseUnsignedNumber(1); -}; - -UseOldPasswordPacket.prototype.write = function(writer) { - writer.writeUnsignedNumber(1, this.firstByte); -}; diff --git a/lib/protocol/packets/index.js b/lib/protocol/packets/index.js index 5c63df53a..4684d626c 100644 --- a/lib/protocol/packets/index.js +++ b/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'); @@ -17,4 +19,3 @@ exports.ResultSetHeaderPacket = require('./ResultSetHeaderPacket'); exports.RowDataPacket = require('./RowDataPacket'); exports.SSLRequestPacket = require('./SSLRequestPacket'); exports.StatisticsPacket = require('./StatisticsPacket'); -exports.UseOldPasswordPacket = require('./UseOldPasswordPacket'); diff --git a/lib/protocol/sequences/Handshake.js b/lib/protocol/sequences/Handshake.js index 3ce49407b..6c84df425 100644 --- a/lib/protocol/sequences/Handshake.js +++ b/lib/protocol/sequences/Handshake.js @@ -25,7 +25,7 @@ Handshake.prototype.determinePacket = function(firstByte) { } if (firstByte === 0xfe) { - return Packets.UseOldPasswordPacket; + return Packets.AuthenticationMethodSwitchRequestPacket; } return undefined; @@ -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) { diff --git a/test/FakeServer.js b/test/FakeServer.js index d5fda2a51..a1bbf52b7 100644 --- a/test/FakeServer.js +++ b/test/FakeServer.js @@ -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)); @@ -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); @@ -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: @@ -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; diff --git a/test/unit/connection/test-force-auth-switch.js b/test/unit/connection/test-force-auth-switch.js new file mode 100644 index 000000000..36c332bdf --- /dev/null +++ b/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); +});