From b4fec1d89acde51156d7c3f30bdf7af5dd13f0bf Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Sat, 13 May 2017 23:06:48 -0700 Subject: [PATCH] Support authentication switch request. Fixes #1396 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). --- lib/protocol/Parser.js | 5 +++ ...AuthenticationMethodSwitchRequestPacket.js | 22 +++++++++ .../AuthenticationSwitchResponsePacket.js | 14 ++++++ lib/protocol/packets/UseOldPasswordPacket.js | 14 ------ lib/protocol/packets/index.js | 3 +- lib/protocol/sequences/Handshake.js | 45 ++++++++++++++----- test/FakeServer.js | 2 +- 7 files changed, 78 insertions(+), 27 deletions(-) create mode 100644 lib/protocol/packets/AuthenticationMethodSwitchRequestPacket.js create mode 100644 lib/protocol/packets/AuthenticationSwitchResponsePacket.js delete mode 100644 lib/protocol/packets/UseOldPasswordPacket.js 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..62d2840ca 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.authPluginData; + 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..9bf819f9b 100644 --- a/test/FakeServer.js +++ b/test/FakeServer.js @@ -268,7 +268,7 @@ 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.password === 'passwd') { var expected = Buffer.from('3DA0ADA7C9E1BB3A110575DF53306F9D2DE7FD09', 'hex'); this._sendAuthResponse(packet, expected);