From 46e90daa05c7021d5f6f9fe0ff7e07ea8aa286e5 Mon Sep 17 00:00:00 2001 From: Rui Quelhas Date: Fri, 9 Mar 2018 14:23:36 +0000 Subject: [PATCH 1/4] tests: fix local_infile breaking change on MySQL 8 --- Readme.md | 8 ++++++++ test/integration/connection/test-load-data-infile.js | 3 +++ .../test-multiple-statements-load-data-infile.js | 2 ++ 3 files changed, 13 insertions(+) diff --git a/Readme.md b/Readme.md index 97b1b2dd8..78c174527 100644 --- a/Readme.md +++ b/Readme.md @@ -1377,6 +1377,14 @@ The following flags are sent by default on a new connection: - `SECURE_CONNECTION` - Support native 4.1 authentication. - `TRANSACTIONS` - Asks for the transaction status flags. +The `local_infile` system variable is disabled by default since MySQL 8.0.2, which +means the `LOCAL_FILES` flag will only make sense if the feature is explicitely +enabled on the server. + +```sql +SET GLOBAL local_infile = true; +``` + In addition, the following flag will be sent if the option `multipleStatements` is set to `true`: diff --git a/test/integration/connection/test-load-data-infile.js b/test/integration/connection/test-load-data-infile.js index 4d5cc4285..febfe2e24 100644 --- a/test/integration/connection/test-load-data-infile.js +++ b/test/integration/connection/test-load-data-infile.js @@ -11,6 +11,9 @@ common.getTestConnection(function (err, connection) { common.useTestDb(connection); + // "LOAD DATA LOCAL" is not allowed on MySQL 8 by default + connection.query('SET GLOBAL local_infile = true', assert.ifError); + connection.query([ 'CREATE TEMPORARY TABLE ?? (', '`id` int(11) unsigned NOT NULL AUTO_INCREMENT,', diff --git a/test/integration/connection/test-multiple-statements-load-data-infile.js b/test/integration/connection/test-multiple-statements-load-data-infile.js index cc115863b..52d4cdc20 100644 --- a/test/integration/connection/test-multiple-statements-load-data-infile.js +++ b/test/integration/connection/test-multiple-statements-load-data-infile.js @@ -10,6 +10,8 @@ common.getTestConnection({multipleStatements: true}, function (err, connection) common.useTestDb(connection); + connection.query('SET GLOBAL local_infile = true', assert.ifError); + connection.query([ 'CREATE TEMPORARY TABLE ?? (', '`id` int(11) unsigned NOT NULL AUTO_INCREMENT,', From bf7e5dfb5f8142b12636efb5a19ed3092979353b Mon Sep 17 00:00:00 2001 From: Rui Quelhas Date: Tue, 13 Mar 2018 10:35:04 +0000 Subject: [PATCH 2/4] tests: fix setup duplicate table issue --- .../connection/test-connection-config-flags-affected-rows.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/integration/connection/test-connection-config-flags-affected-rows.js b/test/integration/connection/test-connection-config-flags-affected-rows.js index 9dc658342..7297e586a 100644 --- a/test/integration/connection/test-connection-config-flags-affected-rows.js +++ b/test/integration/connection/test-connection-config-flags-affected-rows.js @@ -12,6 +12,8 @@ common.getTestConnection({flags: '-FOUND_ROWS'}, function (err, connection) { common.useTestDb(connection); + connection.query('DROP TABLE IF EXISTS ??', [table], assert.ifError); + connection.query([ 'CREATE TEMPORARY TABLE ?? (', '`a` int(11) unsigned NOT NULL AUTO_INCREMENT,', From bf16a72850f2aae043c0574a25088f3b55cf46af Mon Sep 17 00:00:00 2001 From: Rui Quelhas Date: Tue, 13 Mar 2018 10:35:19 +0000 Subject: [PATCH 3/4] Add support for caching_sha2_password handshake --- Readme.md | 79 +++++++++++++ lib/ConnectionConfig.js | 4 +- lib/protocol/Auth.js | 38 +++++- lib/protocol/Parser.js | 4 +- lib/protocol/packets/AuthMoreDataPacket.js | 17 +++ .../packets/ClearTextPasswordPacket.js | 8 ++ lib/protocol/packets/ComChangeUserPacket.js | 3 + lib/protocol/packets/FastAuthSuccessPacket.js | 15 +++ .../packets/HandshakeResponse41Packet.js | 12 ++ .../PerformFullAuthenticationPacket.js | 15 +++ lib/protocol/packets/index.js | 5 + lib/protocol/sequences/ChangeUser.js | 39 ++++--- lib/protocol/sequences/Handshake.js | 108 +++++++++++++++++- test/FakeServer.js | 89 +++++++++++++-- test/common.js | 14 ++- test/fixtures/server-public.key | 6 + .../connection/test-load-data-infile.js | 1 - .../test-auth-switch-caching-sha2.js | 36 ++++++ test/unit/connection/test-auth-switch-old.js | 36 ++++++ .../test-caching-sha2-password-fast.js | 36 ++++++ .../test-caching-sha2-password-full-error.js | 31 +++++ ...g-sha2-password-full-insecure-key-8.0.4.js | 53 +++++++++ ...g-sha2-password-full-insecure-key-error.js | 43 +++++++ ...2-password-full-insecure-key-no-padding.js | 51 +++++++++ ...sha2-password-full-insecure-key-padding.js | 52 +++++++++ ...est-caching-sha2-password-full-insecure.js | 45 ++++++++ .../test-caching-sha2-password-full-secure.js | 38 ++++++ ...a2-password-public-key-encryption-error.js | 38 ++++++ ...er.js => test-change-user-caching-sha2.js} | 0 .../connection/test-change-user-native.js | 35 ++++++ test/unit/connection/test-change-user-old.js | 36 ++++++ ...st-password.js => test-native-password.js} | 5 +- 32 files changed, 952 insertions(+), 40 deletions(-) create mode 100644 lib/protocol/packets/AuthMoreDataPacket.js create mode 100644 lib/protocol/packets/ClearTextPasswordPacket.js create mode 100644 lib/protocol/packets/FastAuthSuccessPacket.js create mode 100644 lib/protocol/packets/HandshakeResponse41Packet.js create mode 100644 lib/protocol/packets/PerformFullAuthenticationPacket.js create mode 100644 test/fixtures/server-public.key create mode 100644 test/unit/connection/test-auth-switch-caching-sha2.js create mode 100644 test/unit/connection/test-auth-switch-old.js create mode 100644 test/unit/connection/test-caching-sha2-password-fast.js create mode 100644 test/unit/connection/test-caching-sha2-password-full-error.js create mode 100644 test/unit/connection/test-caching-sha2-password-full-insecure-key-8.0.4.js create mode 100644 test/unit/connection/test-caching-sha2-password-full-insecure-key-error.js create mode 100644 test/unit/connection/test-caching-sha2-password-full-insecure-key-no-padding.js create mode 100644 test/unit/connection/test-caching-sha2-password-full-insecure-key-padding.js create mode 100644 test/unit/connection/test-caching-sha2-password-full-insecure.js create mode 100644 test/unit/connection/test-caching-sha2-password-full-secure.js create mode 100644 test/unit/connection/test-caching-sha2-password-public-key-encryption-error.js rename test/unit/connection/{test-change-user.js => test-change-user-caching-sha2.js} (100%) create mode 100644 test/unit/connection/test-change-user-native.js create mode 100644 test/unit/connection/test-change-user-old.js rename test/unit/connection/{test-password.js => test-native-password.js} (82%) diff --git a/Readme.md b/Readme.md index 78c174527..d76f027dc 100644 --- a/Readme.md +++ b/Readme.md @@ -16,6 +16,7 @@ - [Community](#community) - [Establishing connections](#establishing-connections) - [Connection options](#connection-options) +- [Authentication options](#authentication-options) - [SSL options](#ssl-options) - [Terminating connections](#terminating-connections) - [Pooling connections](#pooling-connections) @@ -235,6 +236,7 @@ issue [#501](https://github.com/mysqljs/mysql/issues/501). (Default: `false`) also possible to blacklist default ones. For more information, check [Connection Flags](#connection-flags). * `ssl`: object with ssl parameters or a string containing name of ssl profile. See [SSL options](#ssl-options). +* `secureAuth`: required to support `caching_sha2_password` handshakes over insecure connections (default behavior on MySQL 8.0.4 or higher). See [Authentication options](#authentication-options). In addition to passing these options as an object, you can also use a url @@ -247,6 +249,82 @@ var connection = mysql.createConnection('mysql://user:pass@host/db?debug=true&ch Note: The query values are first attempted to be parsed as JSON, and if that fails assumed to be plaintext strings. +### Authentication options + +MySQL 8.0 introduces a new default authentication plugin - [`caching_sha2_password`](https://dev.mysql.com/doc/refman/8.0/en/caching-sha2-pluggable-authentication.html). +This is a breaking change from MySQL 5.7 wherein [`mysql_native_password`](https://dev.mysql.com/doc/refman/8.0/en/native-pluggable-authentication.html) was used by default. + +The initial handshake for this plugin will only work if the connection is secure or the server +uses a valid RSA public key for the given type of authentication (both default MySQL 8 settings). +By default, if the connection is not secure, the client will fetch the public key from the server +and use it (alongside a server-generated nonce) to encrypt the password. + +After a sucessful initial handshake, any subsequent handshakes will always work, until the +server shuts down or the password is somehow removed from the server authentication cache. + +The default connection options provide compatibility with both MySQL 5.7 and MySQL 8 servers. + +```js +// default options +var connection = mysql.createConnection({ + ssl : false, + encryptedAuth : true +}); +``` + +If you are in control of the server public key, you can also provide it explicitely and avoid +the additional round-trip. + +```js +var connection = mysql.createConnection({ + ssl : false, + secureAuth : { + key: fs.readFileSync(__dirname + '/mysql-pub.key') + } +}); +``` + +Alternatively to providing just the key, you can provide additional options, in the same +format as [crypto.publicEncrypt](https://nodejs.org/docs/latest-v4.x/api/crypto.html#crypto_crypto_publicencrypt_public_key_buffer), +which means you can also specify the key padding type. + +**Caution** MySQL 8.0.4 specifically requires `RSA_PKCS1_PADDING` whereas MySQL 8.0.11 GA (and above) require `RSA_PKCS1_OAEP_PADDING` (which is the default value). + +```js +var constants = require('constants'); + +var connection = mysql.createConnection({ + ssl : false, + secureAuth : { + key: fs.readFileSync(__dirname + '/mysql-pub.key'), + padding: constants.RSA_PKCS1_PADDING + } +}); +``` + +At least one of these options needs to be enabled for the initial handshake to work. So, the +following flavour will also work. + +```js +var connection = mysql.createConnection({ + ssl : true, // or a valid ssl configuration object + secureAuth : false +}); +``` + +If both `secureAuth` and `ssl` options are disabled, the connection will fail. + +```js +var connection = mysql.createConnection({ + ssl : false, + secureAuth : false +}); + +connection.connect(function (err) { + console.log(err.message); // 'Authentication requires secure connection' +}); +``` + ### SSL options The `ssl` option in the connection options takes a string or an object. When given a string, @@ -1371,6 +1449,7 @@ The following flags are sent by default on a new connection: - `LONG_PASSWORD` - Use the improved version of Old Password Authentication. - `MULTI_RESULTS` - Can handle multiple resultsets for COM_QUERY. - `ODBC` Old; no effect. +- `PLUGIN_AUTH` - Support different authentication plugins. - `PROTOCOL_41` - Uses the 4.1 protocol. - `PS_MULTI_RESULTS` - Can handle multiple resultsets for COM_STMT_EXECUTE. - `RESERVED` - Old flag for the 4.1 protocol. diff --git a/lib/ConnectionConfig.js b/lib/ConnectionConfig.js index 147aa0abb..b2af7ba44 100644 --- a/lib/ConnectionConfig.js +++ b/lib/ConnectionConfig.js @@ -58,6 +58,8 @@ function ConnectionConfig(options) { // Set the client flags var defaultFlags = ConnectionConfig.getDefaultFlags(options); this.clientFlags = ConnectionConfig.mergeFlags(defaultFlags, options.flags); + + this.secureAuth = options.secureAuth !== undefined ? options.secureAuth : true; } ConnectionConfig.mergeFlags = function mergeFlags(defaultFlags, userFlags) { @@ -106,7 +108,7 @@ ConnectionConfig.getDefaultFlags = function getDefaultFlags(options) { '+LONG_PASSWORD', // Use the improved version of Old Password Authentication '+MULTI_RESULTS', // Can handle multiple resultsets for COM_QUERY '+ODBC', // Special handling of ODBC behaviour - '-PLUGIN_AUTH', // Does *NOT* support auth plugins + '+PLUGIN_AUTH', // Supports auth plugins '+PROTOCOL_41', // Uses the 4.1 protocol '+PS_MULTI_RESULTS', // Can handle multiple resultsets for COM_STMT_EXECUTE '+RESERVED', // Unused diff --git a/lib/protocol/Auth.js b/lib/protocol/Auth.js index e00e893a4..32ac0c854 100644 --- a/lib/protocol/Auth.js +++ b/lib/protocol/Auth.js @@ -2,12 +2,23 @@ var Buffer = require('safe-buffer').Buffer; var Crypto = require('crypto'); var Auth = exports; -function sha1(msg) { - var hash = Crypto.createHash('sha1'); +function createHash(msg, algorithm) { + algorithm = algorithm || 'sha1'; + var hash = Crypto.createHash(algorithm); hash.update(msg, 'binary'); return hash.digest('binary'); } + +function sha1(msg) { + return createHash(msg, 'sha1'); +} + +function sha256(msg) { + return createHash(msg, 'sha256'); +} + Auth.sha1 = sha1; +Auth.sha256 = sha256; function xor(a, b) { a = Buffer.from(a, 'binary'); @@ -32,6 +43,29 @@ Auth.token = function(password, scramble) { return xor(stage3, stage1); }; +Auth.sha2Token = function(password, scramble) { + if (!password) { + return Buffer.alloc(0); + } + + // password must be in binary format, not utf8 + var stage1 = sha256((Buffer.from(password, 'utf8')).toString('binary')); + var stage2 = sha256(stage1); + var stage3 = sha256(stage2 + scramble.toString('binary')); + return xor(stage1, stage3); +}; + +Auth.encrypt = function(password, scramble, key) { + if (typeof Crypto.publicEncrypt !== 'function') { + var err = new Error('The Node.js version does not support public key encryption'); + err.code = 'PUB_KEY_ENCRYPTION_NOT_AVAILABLE'; + throw err; + } + + var stage1 = xor((Buffer.from(password + '\0', 'utf8')).toString('binary'), scramble.toString('binary')); + return Crypto.publicEncrypt(key, stage1); +}; + // This is a port of sql/password.c:hash_password which needs to be used for // pre-4.1 passwords. Auth.hashPassword = function(password) { diff --git a/lib/protocol/Parser.js b/lib/protocol/Parser.js index 9122f1add..07623a18e 100644 --- a/lib/protocol/Parser.js +++ b/lib/protocol/Parser.js @@ -167,8 +167,8 @@ Parser.prototype.resume = function() { process.nextTick(this.write.bind(this)); }; -Parser.prototype.peak = function() { - return this._buffer[this._offset]; +Parser.prototype.peak = function(offset) { + return this._buffer[this._offset + (offset || 0)]; }; Parser.prototype.parseUnsignedNumber = function parseUnsignedNumber(bytes) { diff --git a/lib/protocol/packets/AuthMoreDataPacket.js b/lib/protocol/packets/AuthMoreDataPacket.js new file mode 100644 index 000000000..b1ea77612 --- /dev/null +++ b/lib/protocol/packets/AuthMoreDataPacket.js @@ -0,0 +1,17 @@ +module.exports = AuthMoreDataPacket; +function AuthMoreDataPacket(options) { + options = options || {}; + + this.status = 0x01; + this.data = options.data; +} + +AuthMoreDataPacket.prototype.parse = function parse(parser) { + this.status = parser.parseUnsignedNumber(1); + this.data = parser.parsePacketTerminatedString(); +}; + +AuthMoreDataPacket.prototype.write = function parse(writer) { + writer.writeUnsignedNumber(this.status); + writer.writeString(this.data); +}; diff --git a/lib/protocol/packets/ClearTextPasswordPacket.js b/lib/protocol/packets/ClearTextPasswordPacket.js new file mode 100644 index 000000000..df2e80a48 --- /dev/null +++ b/lib/protocol/packets/ClearTextPasswordPacket.js @@ -0,0 +1,8 @@ +module.exports = ClearTextPasswordPacket; +function ClearTextPasswordPacket(options) { + this.data = options.data; +} + +ClearTextPasswordPacket.prototype.write = function write(writer) { + writer.writeNullTerminatedString(this.data); +}; diff --git a/lib/protocol/packets/ComChangeUserPacket.js b/lib/protocol/packets/ComChangeUserPacket.js index 327884235..92762e211 100644 --- a/lib/protocol/packets/ComChangeUserPacket.js +++ b/lib/protocol/packets/ComChangeUserPacket.js @@ -7,6 +7,7 @@ function ComChangeUserPacket(options) { this.scrambleBuff = options.scrambleBuff; this.database = options.database; this.charsetNumber = options.charsetNumber; + this.authPlugin = options.authPlugin; } ComChangeUserPacket.prototype.parse = function(parser) { @@ -15,6 +16,7 @@ ComChangeUserPacket.prototype.parse = function(parser) { this.scrambleBuff = parser.parseLengthCodedBuffer(); this.database = parser.parseNullTerminatedString(); this.charsetNumber = parser.parseUnsignedNumber(1); + this.authPlugin = parser.parseNullTerminatedString(); }; ComChangeUserPacket.prototype.write = function(writer) { @@ -23,4 +25,5 @@ ComChangeUserPacket.prototype.write = function(writer) { writer.writeLengthCodedBuffer(this.scrambleBuff); writer.writeNullTerminatedString(this.database); writer.writeUnsignedNumber(2, this.charsetNumber); + writer.writeNullTerminatedString(this.authPlugin); }; diff --git a/lib/protocol/packets/FastAuthSuccessPacket.js b/lib/protocol/packets/FastAuthSuccessPacket.js new file mode 100644 index 000000000..2eb77b737 --- /dev/null +++ b/lib/protocol/packets/FastAuthSuccessPacket.js @@ -0,0 +1,15 @@ +module.exports = FastAuthSuccessPacket; +function FastAuthSuccessPacket() { + this.status = 0x01; + this.authMethodName = 0x03; +} + +FastAuthSuccessPacket.prototype.parse = function parse(parser) { + this.status = parser.parseUnsignedNumber(1); + this.authMethodName = parser.parseUnsignedNumber(1); +}; + +FastAuthSuccessPacket.prototype.write = function write(writer) { + writer.writeUnsignedNumber(1, this.status); + writer.writeUnsignedNumber(1, this.authMethodName); +}; diff --git a/lib/protocol/packets/HandshakeResponse41Packet.js b/lib/protocol/packets/HandshakeResponse41Packet.js new file mode 100644 index 000000000..fd933943e --- /dev/null +++ b/lib/protocol/packets/HandshakeResponse41Packet.js @@ -0,0 +1,12 @@ +module.exports = HandshakeResponse41Packet; +function HandshakeResponse41Packet() { + this.status = 0x02; +} + +HandshakeResponse41Packet.prototype.parse = function write(parser) { + this.status = parser.parseUnsignedNumber(1); +}; + +HandshakeResponse41Packet.prototype.write = function write(writer) { + writer.writeUnsignedNumber(1, this.status); +}; diff --git a/lib/protocol/packets/PerformFullAuthenticationPacket.js b/lib/protocol/packets/PerformFullAuthenticationPacket.js new file mode 100644 index 000000000..801b7e33a --- /dev/null +++ b/lib/protocol/packets/PerformFullAuthenticationPacket.js @@ -0,0 +1,15 @@ +module.exports = PerformFullAuthenticationPacket; +function PerformFullAuthenticationPacket() { + this.status = 0x01; + this.authMethodName = 0x04; +} + +PerformFullAuthenticationPacket.prototype.parse = function parse(parser) { + this.status = parser.parseUnsignedNumber(1); + this.authMethodName = parser.parseUnsignedNumber(1); +}; + +PerformFullAuthenticationPacket.prototype.write = function write(writer) { + writer.writeUnsignedNumber(1, this.status); + writer.writeUnsignedNumber(1, this.authMethodName); +}; diff --git a/lib/protocol/packets/index.js b/lib/protocol/packets/index.js index f36b87bfb..bcc435c7d 100644 --- a/lib/protocol/packets/index.js +++ b/lib/protocol/packets/index.js @@ -1,5 +1,7 @@ +exports.AuthMoreDataPacket = require('./AuthMoreDataPacket'); exports.AuthSwitchRequestPacket = require('./AuthSwitchRequestPacket'); exports.AuthSwitchResponsePacket = require('./AuthSwitchResponsePacket'); +exports.ClearTextPasswordPacket = require('./ClearTextPasswordPacket'); exports.ClientAuthenticationPacket = require('./ClientAuthenticationPacket'); exports.ComChangeUserPacket = require('./ComChangeUserPacket'); exports.ComPingPacket = require('./ComPingPacket'); @@ -9,12 +11,15 @@ exports.ComStatisticsPacket = require('./ComStatisticsPacket'); exports.EmptyPacket = require('./EmptyPacket'); exports.EofPacket = require('./EofPacket'); exports.ErrorPacket = require('./ErrorPacket'); +exports.FastAuthSuccessPacket = require('./FastAuthSuccessPacket'); exports.Field = require('./Field'); exports.FieldPacket = require('./FieldPacket'); exports.HandshakeInitializationPacket = require('./HandshakeInitializationPacket'); +exports.HandshakeResponse41Packet = require('./HandshakeResponse41Packet'); exports.LocalDataFilePacket = require('./LocalDataFilePacket'); exports.OkPacket = require('./OkPacket'); exports.OldPasswordPacket = require('./OldPasswordPacket'); +exports.PerformFullAuthenticationPacket = require('./PerformFullAuthenticationPacket'); exports.ResultSetHeaderPacket = require('./ResultSetHeaderPacket'); exports.RowDataPacket = require('./RowDataPacket'); exports.SSLRequestPacket = require('./SSLRequestPacket'); diff --git a/lib/protocol/sequences/ChangeUser.js b/lib/protocol/sequences/ChangeUser.js index 26be6dbbd..40b37db19 100644 --- a/lib/protocol/sequences/ChangeUser.js +++ b/lib/protocol/sequences/ChangeUser.js @@ -1,29 +1,40 @@ -var Sequence = require('./Sequence'); -var Util = require('util'); -var Packets = require('../packets'); -var Auth = require('../Auth'); +var Handshake = require('./Handshake'); +var Util = require('util'); +var Packets = require('../packets'); +var Auth = require('../Auth'); module.exports = ChangeUser; -Util.inherits(ChangeUser, Sequence); +Util.inherits(ChangeUser, Handshake); function ChangeUser(options, callback) { - Sequence.call(this, options, callback); + Handshake.call(this, {config: options}, callback); - this._user = options.user; - this._password = options.password; - this._database = options.database; - this._charsetNumber = options.charsetNumber; - this._currentConfig = options.currentConfig; + this._user = options.user; + this._password = options.password; + this._database = options.database; + this._charsetNumber = options.charsetNumber; + this._currentConfig = options.currentConfig; + this._handshakeInitializationPacket = null; } ChangeUser.prototype.start = function(handshakeInitializationPacket) { - var scrambleBuff = handshakeInitializationPacket.scrambleBuff(); - scrambleBuff = Auth.token(this._password, scrambleBuff); + this._handshakeInitializationPacket = handshakeInitializationPacket; + + var scrambleBuff = this._handshakeInitializationPacket.scrambleBuff(); + + if (this._handshakeInitializationPacket.pluginData === 'caching_sha2_password') { + scrambleBuff = Auth.sha2Token(this._password, scrambleBuff); + } else if (this._handshakeInitializationPacket.pluginData === 'mysql_native_password') { + scrambleBuff = Auth.token(this._password, scrambleBuff); + } else { + scrambleBuff = Auth.scramble323(scrambleBuff, this._password); + } var packet = new Packets.ComChangeUserPacket({ user : this._user, scrambleBuff : scrambleBuff, database : this._database, - charsetNumber : this._charsetNumber + charsetNumber : this._charsetNumber, + authPlugin : this._currentConfig.pluginData }); this._currentConfig.user = this._user; diff --git a/lib/protocol/sequences/Handshake.js b/lib/protocol/sequences/Handshake.js index 2881be844..1003c6b98 100644 --- a/lib/protocol/sequences/Handshake.js +++ b/lib/protocol/sequences/Handshake.js @@ -3,6 +3,7 @@ var Util = require('util'); var Packets = require('../packets'); var Auth = require('../Auth'); var ClientConstants = require('../constants/client'); +var Constants = require('constants'); module.exports = Handshake; Util.inherits(Handshake, Sequence); @@ -13,9 +14,15 @@ function Handshake(options, callback) { this._config = options.config; this._handshakeInitializationPacket = null; + this._waitingForServerPublicKey = false; } Handshake.prototype.determinePacket = function determinePacket(firstByte, parser) { + if (this._waitingForServerPublicKey) { + this._waitingForServerPublicKey = false; + return Packets.AuthMoreDataPacket; + } + if (firstByte === 0xff) { return Packets.ErrorPacket; } @@ -30,16 +37,40 @@ Handshake.prototype.determinePacket = function determinePacket(firstByte, parser : Packets.AuthSwitchRequestPacket; } + if (firstByte === 0x01) { + var secondByte = parser.peak(1); + + if (secondByte === 0x03) { + return Packets.FastAuthSuccessPacket; + } + + if (secondByte === 0x04) { + return Packets.PerformFullAuthenticationPacket; + } + } + return undefined; }; Handshake.prototype['AuthSwitchRequestPacket'] = function (packet) { - if (packet.authMethodName === 'mysql_native_password') { + if (packet.authMethodName === 'caching_sha2_password') { + var challenge = packet.authMethodData.slice(0, 20); + + this.emit('packet', new Packets.AuthSwitchResponsePacket({ + data: Auth.sha2Token(this._config.password, challenge) + })); + } else if (packet.authMethodName === 'mysql_native_password') { var challenge = packet.authMethodData.slice(0, 20); this.emit('packet', new Packets.AuthSwitchResponsePacket({ data: Auth.token(this._config.password, challenge) })); + } else if (packet.authMethodName === 'mysql_old_password') { + var challenge = packet.authMethodData.slice(0, 20); + + this.emit('packet', new Packets.AuthSwitchResponsePacket({ + data: Auth.scramble323(challenge, this._config.password) + })); } else { var err = new Error( 'MySQL is requesting the ' + packet.authMethodName + ' authentication method, which is not supported.' @@ -88,6 +119,16 @@ Handshake.prototype._tlsUpgradeCompleteHandler = function() { Handshake.prototype._sendCredentials = function() { var packet = this._handshakeInitializationPacket; + var scrambleBuff = null; + + if (packet.protocol41 && packet.pluginData === 'caching_sha2_password') { + scrambleBuff = Auth.sha2Token(this._config.password, packet.scrambleBuff()); + } else if (packet.protocol41 && packet.pluginData === 'mysql_native_password') { + scrambleBuff = Auth.token(this._config.password, packet.scrambleBuff()); + } else { + scrambleBuff = Auth.scramble323(packet.scrambleBuff(), this._config.password); + } + this.emit('packet', new Packets.ClientAuthenticationPacket({ clientFlags : this._config.clientFlags, maxPacketSize : this._config.maxPacketSize, @@ -95,9 +136,7 @@ Handshake.prototype._sendCredentials = function() { user : this._config.user, database : this._config.database, protocol41 : packet.protocol41, - scrambleBuff : (packet.protocol41) - ? Auth.token(this._config.password, packet.scrambleBuff()) - : Auth.scramble323(packet.scrambleBuff(), this._config.password) + scrambleBuff : scrambleBuff })); }; @@ -120,6 +159,67 @@ Handshake.prototype['UseOldPasswordPacket'] = function() { })); }; +Handshake.prototype['FastAuthSuccessPacket'] = function() { + // Just to signal an upcoming OkPacket. +}; + +Handshake.prototype['PerformFullAuthenticationPacket'] = function() { + var password = this._config.password; + + if (!this._config.ssl && !this._config.socketPath) { + var secureAuth = this._config.secureAuth; + + if (secureAuth === true || secureAuth && secureAuth.key === true) { + // Fetch the authentication RSA public key from the server. + this._waitingForServerPublicKey = true; + this.emit('packet', new Packets.HandshakeResponse41Packet()); + return; + } + + if (secureAuth && typeof secureAuth.key === 'string') { + // Use the provided authentication RSA public key with any given padding type. + this.AuthMoreDataPacket({ data: secureAuth.key }); + return; + } + + var err = new Error('Authentication requires secure connection'); + + err.code = 'HANDSHAKE_SECURE_TRANSPORT_REQUIRED'; + err.fatal = true; + + this.end(err); + return; + } + + this.emit('packet', new Packets.ClearTextPasswordPacket({ + data: password + })); +}; + +Handshake.prototype['AuthMoreDataPacket'] = function(packet) { + var secureAuth = { key: packet.data, padding: this._config.secureAuth.padding || Constants.RSA_PKCS1_OAEP_PADDING }; + + try { + var password = Auth.encrypt(this._config.password, this._handshakeInitializationPacket.scrambleBuff(), secureAuth); + + this.emit('packet', new Packets.AuthSwitchResponsePacket({ + data: password + })); + } catch (err) { + if (err.code !== 'PUB_KEY_ENCRYPTION_NOT_AVAILABLE') { + throw err; + } + + var error = new Error('Authentication requires secure connection'); + + error.code = 'HANDSHAKE_SECURE_TRANSPORT_REQUIRED'; + error.fatal = true; + + this.end(error); + return; + } +}; + Handshake.prototype['ErrorPacket'] = function(packet) { var err = this._packetToError(packet, true); err.fatal = true; diff --git a/test/FakeServer.js b/test/FakeServer.js index 6b516c024..0208798ab 100644 --- a/test/FakeServer.js +++ b/test/FakeServer.js @@ -72,15 +72,19 @@ function FakeConnection(socket) { FakeConnection.prototype.handshake = function(options) { this._handshakeOptions = options || {}; + this._handshakeOptions.pluginData = this._handshakeOptions.pluginData || + this._handshakeOptions.authMethodName || 'caching_sha2_password'; + this._handshakeOptions.secureAuth = this._handshakeOptions.secureAuth !== undefined + ? this._handshakeOptions.secureAuth : true; - var packetOpiotns = common.extend({ + var packetOptions = common.extend({ scrambleBuff1 : Buffer.from('1020304050607080', 'hex'), scrambleBuff2 : Buffer.from('0102030405060708090A0B0C', 'hex'), serverCapabilities1 : 512, // only 1 flag, PROTOCOL_41 protocol41 : true }, this._handshakeOptions); - this._handshakeInitializationPacket = new Packets.HandshakeInitializationPacket(packetOpiotns); + this._handshakeInitializationPacket = new Packets.HandshakeInitializationPacket(packetOptions); this._sendPacket(this._handshakeInitializationPacket); }; @@ -92,19 +96,39 @@ FakeConnection.prototype.deny = function(message, errno) { })); }; +FakeConnection.prototype._sendError = function _sendError(got, expected) { + this._sendPacket(new Packets.ErrorPacket({ + message : 'expected ' + expected.toString('hex') + ' got ' + got.toString('hex'), + errno : Errors.ER_ACCESS_DENIED_ERROR + })); +}; + FakeConnection.prototype._sendAuthResponse = function _sendAuthResponse(got, expected) { if (expected.toString('hex') === got.toString('hex')) { this._sendPacket(new Packets.OkPacket()); } else { - this._sendPacket(new Packets.ErrorPacket({ - message : 'expected ' + expected.toString('hex') + ' got ' + got.toString('hex'), - errno : Errors.ER_ACCESS_DENIED_ERROR - })); + this._sendError(got, expected); } this._parser.resetPacketNumber(); }; +FakeConnection.prototype._sendEncryptedAuthResponse = function _sendEncryptedAuthResponse(got, expected) { + if (expected.length === got.length) { + this._sendPacket(new Packets.OkPacket()); + } else { + this._sendError(got, expected); + } +}; + +FakeConnection.prototype._resetAuthProcess = function _resetAuthProcess(got, expected) { + if (expected.toString('hex') === got.toString('hex')) { + this._sendPacket(new Packets.PerformFullAuthenticationPacket()); + } else { + this._sendError(got, expected); + } +}; + FakeConnection.prototype._sendPacket = function(packet) { var writer = new PacketWriter(); packet.write(writer); @@ -268,11 +292,22 @@ FakeConnection.prototype._parsePacket = function(header) { this._clientAuthenticationPacket = packet; if (this._handshakeOptions.oldPassword) { this._sendPacket(new Packets.UseOldPasswordPacket()); - } else if (this._handshakeOptions.authMethodName) { - this._sendPacket(new Packets.AuthSwitchRequestPacket(this._handshakeOptions)); - } else if (this._handshakeOptions.password === 'passwd') { + } else if (this._handshakeOptions.authMethodName === 'mysql_native_password' + && this._handshakeOptions.password === 'passwd') { var expected = Buffer.from('3DA0ADA7C9E1BB3A110575DF53306F9D2DE7FD09', 'hex'); this._sendAuthResponse(packet.scrambleBuff, expected); + } else if (this._handshakeOptions.authMethodName === 'caching_sha2_password' + && this._handshakeOptions.authSwitchType === 'perform_full_authentication' + && this._handshakeOptions.password === 'passwd') { + var expected = Buffer.from('490572A245DB857BD76651BF54F350496006B4791334183B8F11ABC9E3D48F9A', 'hex'); + this._resetAuthProcess(packet.scrambleBuff, expected); + } else if (this._handshakeOptions.authMethodName === 'caching_sha2_password' + && this._handshakeOptions.authSwitchType === 'fast_auth_success' + && this._handshakeOptions.password === 'passwd') { + this._sendPacket(new Packets.FastAuthSuccessPacket()); + this._sendPacket(new Packets.OkPacket()); + } else if (this._handshakeOptions.authMethodName) { + this._sendPacket(new Packets.AuthSwitchRequestPacket(this._handshakeOptions)); } else if (this._handshakeOptions.user || this._handshakeOptions.password) { throw new Error('not implemented'); } else { @@ -293,9 +328,30 @@ FakeConnection.prototype._parsePacket = function(header) { case Packets.AuthSwitchResponsePacket: this._authSwitchResponse = packet; - var expected = Auth.token(this._handshakeOptions.password, Buffer.from('00112233445566778899AABBCCDDEEFF01020304', 'hex')); + var expected = null; + var password = this._handshakeOptions.password; + var nonce = Buffer.from('00112233445566778899AABBCCDDEEFF01020304', 'hex'); + + var isHandshake = this._handshakeOptions.authMethodName === 'caching_sha2_password' + && this._handshakeOptions.authSwitchType === 'perform_full_authentication'; + + if (isHandshake && !this._handshakeOptions.secureAuth) { + // Password should be sent as plain text. + expected = Buffer.from(password + '\0', 'utf8'); + this._sendAuthResponse(packet.data, expected); + } else if (isHandshake && this._handshakeOptions.secureAuth) { + // Password should be encrypted using the server public key. + nonce = this._handshakeInitializationPacket.scrambleBuff(); + expected = Auth.encrypt(password, nonce, this._handshakeOptions.secureAuth); + this._sendEncryptedAuthResponse(packet.data, expected); + } else if (this._handshakeOptions.authMethodName === 'caching_sha2_password') { + expected = Auth.sha2Token(password, nonce); + this._sendAuthResponse(packet.data, expected); + } else { + expected = Auth.token(password, nonce); + this._sendAuthResponse(packet.data, expected); + } - this._sendAuthResponse(packet.data, expected); break; case Packets.ComQueryPacket: if (!this.emit('query', packet)) { @@ -343,6 +399,11 @@ FakeConnection.prototype._parsePacket = function(header) { this._socket.end(); } break; + case Packets.HandshakeResponse41Packet: + this._sendPacket(new Packets.AuthMoreDataPacket({ + data: common.getServerPublicKey() + })); + break; default: throw new Error('Unexpected packet: ' + Packet.name); } @@ -364,6 +425,12 @@ FakeConnection.prototype._determinePacket = function(header) { } if (this._handshakeOptions.authMethodName && !this._authSwitchResponse) { + var secureAuth = this._handshakeOptions.secureAuth; + + if (secureAuth === true || secureAuth && secureAuth.key === true) { + return Packets.HandshakeResponse41Packet; + } + return Packets.AuthSwitchResponsePacket; } diff --git a/test/common.js b/test/common.js index db502b60f..d3a56b8cd 100644 --- a/test/common.js +++ b/test/common.js @@ -1,6 +1,7 @@ -var common = exports; -var fs = require('fs'); -var path = require('path'); +var common = exports; +var fs = require('fs'); +var path = require('path'); +var constants = require('constants'); common.lib = path.resolve(__dirname, '..', 'lib'); common.fixtures = path.resolve(__dirname, 'fixtures'); @@ -31,6 +32,9 @@ common.PoolConnection = require(common.lib + '/PoolConnection'); common.SqlString = require(common.lib + '/protocol/SqlString'); common.Types = require(common.lib + '/protocol/constants/types'); +// Export Node.js constants +common.PlatformConstants = constants; + var Mysql = require(path.resolve(common.lib, '../index')); var FakeServer = require('./FakeServer'); @@ -146,6 +150,10 @@ common.getSSLConfig = function() { }; }; +common.getServerPublicKey = function() { + return fs.readFileSync(path.join(common.fixtures, 'server-public.key'), 'ascii'); +}; + function mergeTestConfig(config) { config = common.extend({ host : process.env.MYSQL_HOST, diff --git a/test/fixtures/server-public.key b/test/fixtures/server-public.key new file mode 100644 index 000000000..68a7b847b --- /dev/null +++ b/test/fixtures/server-public.key @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDX4dUmhwwzqy8zCrNK5WybifZ4 +Z5vd12CnnrBpLgqw0VWiKa2bQQ5vmex4LhEPwr5p+tDntS1sbQf4HY69AgjHtcPA +doykWtUBDCrOjnhEBroLGzE2BbW0XnolsWUp8Zwnlq3nGOceCcvYX3AjUkK89B3L +7YY+Rie/1QQ62FSS2wIDAQAB +-----END PUBLIC KEY----- diff --git a/test/integration/connection/test-load-data-infile.js b/test/integration/connection/test-load-data-infile.js index febfe2e24..bb9bbd2f5 100644 --- a/test/integration/connection/test-load-data-infile.js +++ b/test/integration/connection/test-load-data-infile.js @@ -11,7 +11,6 @@ common.getTestConnection(function (err, connection) { common.useTestDb(connection); - // "LOAD DATA LOCAL" is not allowed on MySQL 8 by default connection.query('SET GLOBAL local_infile = true', assert.ifError); connection.query([ diff --git a/test/unit/connection/test-auth-switch-caching-sha2.js b/test/unit/connection/test-auth-switch-caching-sha2.js new file mode 100644 index 000000000..72a5e6435 --- /dev/null +++ b/test/unit/connection/test-auth-switch-caching-sha2.js @@ -0,0 +1,36 @@ +var assert = require('assert'); +var Buffer = require('safe-buffer').Buffer; +var common = require('../../common'); +var connection = common.createConnection({ + port : common.fakeServerPort, + password : 'authswitch' +}); + +var server = common.createFakeServer(); + +var connected; +server.listen(common.fakeServerPort, function (err) { + assert.ifError(err); + + connection.connect(function (err, result) { + assert.ifError(err); + + connected = result; + + connection.destroy(); + server.destroy(); + }); +}); + +server.on('connection', function(incomingConnection) { + incomingConnection.handshake({ + user : connection.config.user, + password : connection.config.password, + authMethodName : 'caching_sha2_password', + authMethodData : Buffer.from('00112233445566778899AABBCCDDEEFF0102030400', 'hex') + }); +}); + +process.on('exit', function() { + assert.equal(connected.fieldCount, 0); +}); diff --git a/test/unit/connection/test-auth-switch-old.js b/test/unit/connection/test-auth-switch-old.js new file mode 100644 index 000000000..64d859ab1 --- /dev/null +++ b/test/unit/connection/test-auth-switch-old.js @@ -0,0 +1,36 @@ +var assert = require('assert'); +var Buffer = require('safe-buffer').Buffer; +var common = require('../../common'); +var connection = common.createConnection({ + port : common.fakeServerPort, + password : 'authswitch' +}); + +var server = common.createFakeServer(); + +var connected; +server.listen(common.fakeServerPort, function (err) { + assert.ifError(err); + + connection.connect(function (err, result) { + assert.ifError(err); + + connected = result; + + connection.destroy(); + server.destroy(); + }); +}); + +server.on('connection', function(incomingConnection) { + incomingConnection.handshake({ + user : connection.config.user, + password : connection.config.password, + authMethodName : 'mysql_old_password', + authMethodData : Buffer.from('00112233445566778899AABBCCDDEEFF0102030400', 'hex') + }); +}); + +process.on('exit', function() { + assert.equal(connected.fieldCount, 0); +}); diff --git a/test/unit/connection/test-caching-sha2-password-fast.js b/test/unit/connection/test-caching-sha2-password-fast.js new file mode 100644 index 000000000..ed001f17b --- /dev/null +++ b/test/unit/connection/test-caching-sha2-password-fast.js @@ -0,0 +1,36 @@ +var assert = require('assert'); +var common = require('../../common'); +var connection = common.createConnection({ + port : common.fakeServerPort, + password : 'passwd' +}); + +var server = common.createFakeServer(); + +var connected; +server.listen(common.fakeServerPort, function (err) { + assert.ifError(err); + + connection.connect(function (err, result) { + assert.ifError(err); + + connected = result; + + connection.destroy(); + server.destroy(); + }); +}); + +server.on('connection', function(incomingConnection) { + incomingConnection.handshake({ + serverCapabilities1 : common.ClientConstants.CLIENT_SSL, + user : connection.config.user, + password : connection.config.password, + authMethodName : 'caching_sha2_password', + authSwitchType : 'fast_auth_success' + }); +}); + +process.on('exit', function() { + assert.equal(connected.fieldCount, 0); +}); diff --git a/test/unit/connection/test-caching-sha2-password-full-error.js b/test/unit/connection/test-caching-sha2-password-full-error.js new file mode 100644 index 000000000..6d47c75d6 --- /dev/null +++ b/test/unit/connection/test-caching-sha2-password-full-error.js @@ -0,0 +1,31 @@ +var assert = require('assert'); +var common = require('../../common'); +var connection = common.createConnection({ + port : common.fakeServerPort, + password : 'passwd', + secureAuth : false +}); + +var server = common.createFakeServer(); + +server.listen(common.fakeServerPort, function (err) { + assert.ifError(err); + + connection.connect(function (err) { + assert.ok(err, 'got error'); + assert.equal(err.code, 'HANDSHAKE_SECURE_TRANSPORT_REQUIRED'); + assert.ok(err.fatal); + + connection.destroy(); + server.destroy(); + }); +}); + +server.on('connection', function(incomingConnection) { + incomingConnection.handshake({ + user : connection.config.user, + password : connection.config.password, + authMethodName : 'caching_sha2_password', + authSwitchType : 'perform_full_authentication' + }); +}); diff --git a/test/unit/connection/test-caching-sha2-password-full-insecure-key-8.0.4.js b/test/unit/connection/test-caching-sha2-password-full-insecure-key-8.0.4.js new file mode 100644 index 000000000..96e758846 --- /dev/null +++ b/test/unit/connection/test-caching-sha2-password-full-insecure-key-8.0.4.js @@ -0,0 +1,53 @@ +var Buffer = require('safe-buffer').Buffer; +var assert = require('assert'); +var crypto = require('crypto'); +var common = require('../../common'); + +var publicEncrypt = crypto.publicEncrypt; +crypto.publicEncrypt = publicEncrypt || function () { + return Buffer.from('passwd', 'utf8'); +}; + +var secureAuth = { + key : common.getServerPublicKey(), + padding : common.PlatformConstants.RSA_PKCS1_OAEP_PADDING +}; + +var connection = common.createConnection({ + port : common.fakeServerPort, + password : 'passwd', + secureAuth : secureAuth +}); + +var server = common.createFakeServer(); + +var connected; +server.listen(common.fakeServerPort, function (err) { + assert.ifError(err); + + connection.connect(function (err, result) { + assert.ifError(err); + + connected = result; + + connection.destroy(); + server.destroy(); + + crypto.publicEncrypt = publicEncrypt; + }); +}); + +server.on('connection', function(incomingConnection) { + incomingConnection.handshake({ + user : connection.config.user, + password : connection.config.password, + authMethodName : 'caching_sha2_password', + authSwitchType : 'perform_full_authentication', + secureAuth : secureAuth, + serverVersion : '8.0.4-rc' + }); +}); + +process.on('exit', function() { + assert.equal(connected.fieldCount, 0); +}); diff --git a/test/unit/connection/test-caching-sha2-password-full-insecure-key-error.js b/test/unit/connection/test-caching-sha2-password-full-insecure-key-error.js new file mode 100644 index 000000000..039b0a908 --- /dev/null +++ b/test/unit/connection/test-caching-sha2-password-full-insecure-key-error.js @@ -0,0 +1,43 @@ +var Buffer = require('safe-buffer').Buffer; +var assert = require('assert'); +var crypto = require('crypto'); +var common = require('../../common'); + +var publicEncrypt = crypto.publicEncrypt; +crypto.publicEncrypt = publicEncrypt || function () { + return Buffer.from('passwd', 'utf8'); +}; + +var connection = common.createConnection({ + port : common.fakeServerPort, + password : 'passwd', + secureAuth : { + padding: common.PlatformConstants.RSA_PKCS1_OAEP_PADDING + } +}); + +var server = common.createFakeServer(); + +server.listen(common.fakeServerPort, function (err) { + assert.ifError(err); + + connection.connect(function (err) { + assert.ok(err, 'got error'); + assert.equal(err.code, 'HANDSHAKE_SECURE_TRANSPORT_REQUIRED'); + assert.ok(err.fatal); + + connection.destroy(); + server.destroy(); + + crypto.publicEncrypt = publicEncrypt; + }); +}); + +server.on('connection', function(incomingConnection) { + incomingConnection.handshake({ + user : connection.config.user, + password : connection.config.password, + authMethodName : 'caching_sha2_password', + authSwitchType : 'perform_full_authentication' + }); +}); diff --git a/test/unit/connection/test-caching-sha2-password-full-insecure-key-no-padding.js b/test/unit/connection/test-caching-sha2-password-full-insecure-key-no-padding.js new file mode 100644 index 000000000..d01eb94ae --- /dev/null +++ b/test/unit/connection/test-caching-sha2-password-full-insecure-key-no-padding.js @@ -0,0 +1,51 @@ +var Buffer = require('safe-buffer').Buffer; +var assert = require('assert'); +var crypto = require('crypto'); +var common = require('../../common'); + +var publicEncrypt = crypto.publicEncrypt; +crypto.publicEncrypt = publicEncrypt || function () { + return Buffer.from('passwd', 'utf8'); +}; + +var secureAuth = { + key: common.getServerPublicKey() +}; + +var connection = common.createConnection({ + port : common.fakeServerPort, + password : 'passwd', + secureAuth : secureAuth +}); + +var server = common.createFakeServer(); + +var connected; +server.listen(common.fakeServerPort, function (err) { + assert.ifError(err); + + connection.connect(function (err, result) { + assert.ifError(err); + + connected = result; + + connection.destroy(); + server.destroy(); + + crypto.publicEncrypt = publicEncrypt; + }); +}); + +server.on('connection', function(incomingConnection) { + incomingConnection.handshake({ + user : connection.config.user, + password : connection.config.password, + authMethodName : 'caching_sha2_password', + authSwitchType : 'perform_full_authentication', + secureAuth : secureAuth + }); +}); + +process.on('exit', function() { + assert.equal(connected.fieldCount, 0); +}); diff --git a/test/unit/connection/test-caching-sha2-password-full-insecure-key-padding.js b/test/unit/connection/test-caching-sha2-password-full-insecure-key-padding.js new file mode 100644 index 000000000..d96371dbe --- /dev/null +++ b/test/unit/connection/test-caching-sha2-password-full-insecure-key-padding.js @@ -0,0 +1,52 @@ +var Buffer = require('safe-buffer').Buffer; +var assert = require('assert'); +var crypto = require('crypto'); +var common = require('../../common'); + +var publicEncrypt = crypto.publicEncrypt; +crypto.publicEncrypt = publicEncrypt || function () { + return Buffer.from('passwd', 'utf8'); +}; + +var secureAuth = { + key : common.getServerPublicKey(), + padding : common.PlatformConstants.RSA_PKCS1_OAEP_PADDING +}; + +var connection = common.createConnection({ + port : common.fakeServerPort, + password : 'passwd', + secureAuth : secureAuth +}); + +var server = common.createFakeServer(); + +var connected; +server.listen(common.fakeServerPort, function (err) { + assert.ifError(err); + + connection.connect(function (err, result) { + assert.ifError(err); + + connected = result; + + connection.destroy(); + server.destroy(); + + crypto.publicEncrypt = publicEncrypt; + }); +}); + +server.on('connection', function(incomingConnection) { + incomingConnection.handshake({ + user : connection.config.user, + password : connection.config.password, + authMethodName : 'caching_sha2_password', + authSwitchType : 'perform_full_authentication', + secureAuth : secureAuth + }); +}); + +process.on('exit', function() { + assert.equal(connected.fieldCount, 0); +}); diff --git a/test/unit/connection/test-caching-sha2-password-full-insecure.js b/test/unit/connection/test-caching-sha2-password-full-insecure.js new file mode 100644 index 000000000..8664c0346 --- /dev/null +++ b/test/unit/connection/test-caching-sha2-password-full-insecure.js @@ -0,0 +1,45 @@ +var Buffer = require('safe-buffer').Buffer; +var assert = require('assert'); +var crypto = require('crypto'); +var common = require('../../common'); + +var publicEncrypt = crypto.publicEncrypt; +crypto.publicEncrypt = publicEncrypt || function () { + return Buffer.from('passwd', 'utf8'); +}; + +var connection = common.createConnection({ + port : common.fakeServerPort, + password : 'passwd' +}); + +var server = common.createFakeServer(); + +var connected; +server.listen(common.fakeServerPort, function (err) { + assert.ifError(err); + + connection.connect(function (err, result) { + assert.ifError(err); + + connected = result; + + connection.destroy(); + server.destroy(); + + crypto.publicEncrypt = publicEncrypt; + }); +}); + +server.on('connection', function(incomingConnection) { + incomingConnection.handshake({ + user : connection.config.user, + password : connection.config.password, + authMethodName : 'caching_sha2_password', + authSwitchType : 'perform_full_authentication' + }); +}); + +process.on('exit', function() { + assert.equal(connected.fieldCount, 0); +}); diff --git a/test/unit/connection/test-caching-sha2-password-full-secure.js b/test/unit/connection/test-caching-sha2-password-full-secure.js new file mode 100644 index 000000000..973110166 --- /dev/null +++ b/test/unit/connection/test-caching-sha2-password-full-secure.js @@ -0,0 +1,38 @@ +var assert = require('assert'); +var common = require('../../common'); +var connection = common.createConnection({ + port : common.fakeServerPort, + password : 'passwd', + ssl : true, + secureAuth : false +}); + +var server = common.createFakeServer(); + +var connected; +server.listen(common.fakeServerPort, function (err) { + assert.ifError(err); + + connection.connect(function (err, result) { + assert.ifError(err); + + connected = result; + + connection.destroy(); + server.destroy(); + }); +}); + +server.on('connection', function(incomingConnection) { + incomingConnection.handshake({ + serverCapabilities1 : common.ClientConstants.CLIENT_SSL, + user : connection.config.user, + password : connection.config.password, + authMethodName : 'caching_sha2_password', + authSwitchType : 'perform_full_authentication' + }); +}); + +process.on('exit', function() { + assert.equal(connected.fieldCount, 0); +}); diff --git a/test/unit/connection/test-caching-sha2-password-public-key-encryption-error.js b/test/unit/connection/test-caching-sha2-password-public-key-encryption-error.js new file mode 100644 index 000000000..59b1de118 --- /dev/null +++ b/test/unit/connection/test-caching-sha2-password-public-key-encryption-error.js @@ -0,0 +1,38 @@ +var assert = require('assert'); +var crypto = require('crypto'); +var common = require('../../common'); + +var publicEncrypt = crypto.publicEncrypt; +crypto.publicEncrypt = undefined; + +var connection = common.createConnection({ + port : common.fakeServerPort, + password : 'passwd', + secureAuth : true +}); + +var server = common.createFakeServer(); + +server.listen(common.fakeServerPort, function (err) { + assert.ifError(err); + + connection.connect(function (err) { + assert.ok(err, 'got error'); + assert.equal(err.code, 'HANDSHAKE_SECURE_TRANSPORT_REQUIRED'); + assert.ok(err.fatal); + + connection.destroy(); + server.destroy(); + + crypto.publicEncrypt = publicEncrypt; + }); +}); + +server.on('connection', function(incomingConnection) { + incomingConnection.handshake({ + user : connection.config.user, + password : connection.config.password, + authMethodName : 'caching_sha2_password', + authSwitchType : 'perform_full_authentication' + }); +}); diff --git a/test/unit/connection/test-change-user.js b/test/unit/connection/test-change-user-caching-sha2.js similarity index 100% rename from test/unit/connection/test-change-user.js rename to test/unit/connection/test-change-user-caching-sha2.js diff --git a/test/unit/connection/test-change-user-native.js b/test/unit/connection/test-change-user-native.js new file mode 100644 index 000000000..82fb0b5e0 --- /dev/null +++ b/test/unit/connection/test-change-user-native.js @@ -0,0 +1,35 @@ +var assert = require('assert'); +var common = require('../../common'); +var connection = common.createConnection({ + port : common.fakeServerPort, + user : 'user_1' +}); + +var server = common.createFakeServer(); + +server.listen(common.fakeServerPort, function(err) { + assert.ifError(err); + + connection.query('SELECT CURRENT_USER()', function (err, result) { + assert.ifError(err); + assert.strictEqual(result[0]['CURRENT_USER()'], 'user_1@localhost'); + + connection.changeUser({user: 'user_2'}, function (err) { + assert.ifError(err); + + connection.query('SELECT CURRENT_USER()', function (err, result) { + assert.ifError(err); + assert.strictEqual(result[0]['CURRENT_USER()'], 'user_2@localhost'); + + connection.destroy(); + server.destroy(); + }); + }); + }); +}); + +server.on('connection', function(incomingConnection) { + incomingConnection.handshake({ + pluginData: 'mysql_native_password' + }); +}); diff --git a/test/unit/connection/test-change-user-old.js b/test/unit/connection/test-change-user-old.js new file mode 100644 index 000000000..22e4c01d3 --- /dev/null +++ b/test/unit/connection/test-change-user-old.js @@ -0,0 +1,36 @@ +var assert = require('assert'); +var common = require('../../common'); +var connection = common.createConnection({ + port : common.fakeServerPort, + user : 'user_1', + password : 'passwd' +}); + +var server = common.createFakeServer(); + +server.listen(common.fakeServerPort, function(err) { + assert.ifError(err); + + connection.query('SELECT CURRENT_USER()', function (err, result) { + assert.ifError(err); + assert.strictEqual(result[0]['CURRENT_USER()'], 'user_1@localhost'); + + connection.changeUser({user: 'user_2'}, function (err) { + assert.ifError(err); + + connection.query('SELECT CURRENT_USER()', function (err, result) { + assert.ifError(err); + assert.strictEqual(result[0]['CURRENT_USER()'], 'user_2@localhost'); + + connection.destroy(); + server.destroy(); + }); + }); + }); +}); + +server.on('connection', function(incomingConnection) { + incomingConnection.handshake({ + pluginData: 'mysql_old_password' + }); +}); diff --git a/test/unit/connection/test-password.js b/test/unit/connection/test-native-password.js similarity index 82% rename from test/unit/connection/test-password.js rename to test/unit/connection/test-native-password.js index 5d92e2876..d686b95e8 100644 --- a/test/unit/connection/test-password.js +++ b/test/unit/connection/test-native-password.js @@ -23,8 +23,9 @@ server.listen(common.fakeServerPort, function(err) { server.on('connection', function(incomingConnection) { incomingConnection.handshake({ - user : connection.config.user, - password : connection.config.password + user : connection.config.user, + password : connection.config.password, + authMethodName : 'mysql_native_password' }); }); From 317fab91effb15dce0571e97b441e9575a41cb14 Mon Sep 17 00:00:00 2001 From: Rui Quelhas Date: Sun, 18 Mar 2018 13:25:34 +0000 Subject: [PATCH 4/4] build: support MySQL 8 series --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.travis.yml b/.travis.yml index 2dd91a872..2f13e0119 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,6 +26,14 @@ matrix: env: "DOCKER_MYSQL_TYPE=mysql DOCKER_MYSQL_VERSION=5.5" - node_js: "6.13" env: "DOCKER_MYSQL_TYPE=mysql DOCKER_MYSQL_VERSION=5.6" + - node_js: "6.13" + env: "DOCKER_MYSQL_TYPE=mysql DOCKER_MYSQL_VERSION=8.0.0" + - node_js: "6.13" + env: "DOCKER_MYSQL_TYPE=mysql DOCKER_MYSQL_VERSION=8.0.2" + - node_js: "6.13" + env: "DOCKER_MYSQL_TYPE=mysql DOCKER_MYSQL_VERSION=8.0.4" + - node_js: "6.13" + env: "DOCKER_MYSQL_TYPE=mysql DOCKER_MYSQL_VERSION=8.0.11" - node_js: "6.13" env: "DOCKER_MYSQL_TYPE=mariadb DOCKER_MYSQL_VERSION=5.5" - node_js: "6.13"