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/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 index d73bf4459..a9508696a 100644 --- a/lib/protocol/packets/UseOldPasswordPacket.js +++ b/lib/protocol/packets/UseOldPasswordPacket.js @@ -2,13 +2,25 @@ module.exports = UseOldPasswordPacket; function UseOldPasswordPacket(options) { options = options || {}; - this.firstByte = options.firstByte || 0xfe; + this.firstByte = options.firstByte || 0xfe; + this.methodName = options.methodName; + this.pluginData = options.pluginData; } UseOldPasswordPacket.prototype.parse = function(parser) { this.firstByte = parser.parseUnsignedNumber(1); + if (!parser.reachedPacketEnd()) { + this.methodName = parser.parseNullTerminatedString(); + this.pluginData = parser.parsePacketTerminatedBuffer(); + } }; UseOldPasswordPacket.prototype.write = function(writer) { writer.writeUnsignedNumber(1, this.firstByte); + if (this.methodName !== undefined) { + writer.writeNullTerminatedString(this.methodName); + if (this.pluginData !== undefined) { + writer.writeBuffer(this.pluginData); + } + } }; diff --git a/lib/protocol/packets/index.js b/lib/protocol/packets/index.js index 5c63df53a..6fbe6ac9c 100644 --- a/lib/protocol/packets/index.js +++ b/lib/protocol/packets/index.js @@ -1,3 +1,4 @@ +exports.AuthenticationSwitchResponsePacket = require('./AuthenticationSwitchResponsePacket'); exports.ClientAuthenticationPacket = require('./ClientAuthenticationPacket'); exports.ComChangeUserPacket = require('./ComChangeUserPacket'); exports.ComPingPacket = require('./ComPingPacket'); diff --git a/lib/protocol/sequences/Handshake.js b/lib/protocol/sequences/Handshake.js index 3ce49407b..41c60c8f2 100644 --- a/lib/protocol/sequences/Handshake.js +++ b/lib/protocol/sequences/Handshake.js @@ -80,23 +80,46 @@ Handshake.prototype._sendCredentials = function() { })); }; -Handshake.prototype['UseOldPasswordPacket'] = function() { - if (!this._config.insecureAuth) { +Handshake.prototype['UseOldPasswordPacket'] = function(packet) { + if (!packet || !packet.methodName || 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 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 { 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..8caae071e 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)); @@ -269,6 +270,8 @@ FakeConnection.prototype._parsePacket = function(header) { this._clientAuthenticationPacket = packet; if (this._handshakeOptions.oldPassword) { this._sendPacket(new Packets.UseOldPasswordPacket()); + } else if (this._handshakeOptions.forceAuthSwitch) { + this._sendPacket(new Packets.UseOldPasswordPacket({ methodName: 'mysql_native_password', 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); +});