From 28d8af17c2a769581bae2cd101bdd8b2b8f9393d 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 3d5069341..a2221b8c3 100644 --- a/Readme.md +++ b/Readme.md @@ -1399,6 +1399,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 08fe20390495bd5ca18e58ebe925f4409fda99cf 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 6c1872b48c7f8de72d6140f2f3993cf83161fc48 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 | 82 +++++++++++++++- lib/ConnectionConfig.js | 4 +- lib/protocol/Auth.js | 42 +++++++- 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 | 60 +++++++++--- lib/protocol/sequences/Handshake.js | 97 ++++++++++++++++++- test/FakeServer.js | 46 ++++++++- test/common.js | 14 ++- test/fixtures/server-public.key | 6 ++ .../connection/test-load-data-infile.js | 1 - .../test-auth-caching-sha2-password-fast.js | 40 ++++++++ ...t-auth-caching-sha2-password-full-error.js | 42 ++++++++ ...th-caching-sha2-password-full-key-error.js | 45 +++++++++ ...ching-sha2-password-full-key-no-padding.js | 61 ++++++++++++ ...-caching-sha2-password-full-key-padding.js | 62 ++++++++++++ ...ching-sha2-password-full-padding-no-key.js | 59 +++++++++++ ...est-auth-caching-sha2-password-full-ssl.js | 60 ++++++++++++ .../test-auth-caching-sha2-password-full.js | 56 +++++++++++ ...a2-password-public-key-encryption-error.js | 46 +++++++++ ...ssword.js => test-auth-native-password.js} | 5 +- .../test-auth-switch-caching-sha2.js | 48 +++++++++ test/unit/connection/test-auth-switch-old.js | 48 +++++++++ ...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 +++++++ 31 files changed, 1042 insertions(+), 28 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-caching-sha2-password-fast.js create mode 100644 test/unit/connection/test-auth-caching-sha2-password-full-error.js create mode 100644 test/unit/connection/test-auth-caching-sha2-password-full-key-error.js create mode 100644 test/unit/connection/test-auth-caching-sha2-password-full-key-no-padding.js create mode 100644 test/unit/connection/test-auth-caching-sha2-password-full-key-padding.js create mode 100644 test/unit/connection/test-auth-caching-sha2-password-full-padding-no-key.js create mode 100644 test/unit/connection/test-auth-caching-sha2-password-full-ssl.js create mode 100644 test/unit/connection/test-auth-caching-sha2-password-full.js create mode 100644 test/unit/connection/test-auth-caching-sha2-password-public-key-encryption-error.js rename test/unit/connection/{test-auth-password.js => test-auth-native-password.js} (87%) create mode 100644 test/unit/connection/test-auth-switch-caching-sha2.js create mode 100644 test/unit/connection/test-auth-switch-old.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 diff --git a/Readme.md b/Readme.md index a2221b8c3..cacaa3d29 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 successful 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, + secureAuth : true +}); +``` + +If you are in control of the server public key, you can also provide it explicitly and avoid +the additional round-trip. + +```js +var connection = mysql.createConnection({ + ssl : false, + secureAuth : { + key: fs.readFileSync(__dirname + '/mysql-pub.key') + } +}); +``` + +As an alternative 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, @@ -560,6 +638,7 @@ The available options for this feature are: * `password`: The password of the new user (defaults to the previous one). * `charset`: The new charset (defaults to the previous one). * `database`: The new database (defaults to the previous one). +* `timeout`: An optional [timeout](#timeouts). A sometimes useful side effect of this functionality is that this function also resets any connection state (variables, transactions, etc.). @@ -611,7 +690,7 @@ connection.query('SELECT * FROM `books` WHERE `author` = ?', ['David'], function The third form `.query(options, callback)` comes when using various advanced options on the query, like [escaping query values](#escaping-query-values), [joins with overlapping column names](#joins-with-overlapping-column-names), -[timeouts](#timeout), and [type casting](#type-casting). +[timeouts](#timeouts), and [type casting](#type-casting). ```js connection.query({ @@ -1393,6 +1472,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 a1033d1b9..3d025e8f9 100644 --- a/lib/protocol/Auth.js +++ b/lib/protocol/Auth.js @@ -6,20 +6,35 @@ function auth(name, data, options) { options = options || {}; switch (name) { + case 'caching_sha2_password': + return Auth.sha2Token(options.password, data.slice(0, 20)); case 'mysql_native_password': return Auth.token(options.password, data.slice(0, 20)); + case 'mysql_old_password': + return Auth.scramble323(data.slice(0, 20), options.password); default: return undefined; } } Auth.auth = auth; -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'); @@ -44,6 +59,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/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 e1cc1fbc3..903acfced 100644 --- a/lib/protocol/sequences/ChangeUser.js +++ b/lib/protocol/sequences/ChangeUser.js @@ -1,18 +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'); + +function createHandshakeOptions(options) { + var config = {}; + var currentConfig = options.currentConfig; + var configKeys = Object.keys(currentConfig); + + for (var i = 0; i < configKeys.length; i++) { + var key = configKeys[i]; + config[key] = currentConfig[key]; + } + + config.user = options.user; + config.password = options.password; + config.database = options.database; + config.charsetNumber = options.charsetNumber; + + return { + config : config, + timeout : options.timeout + }; +} module.exports = ChangeUser; -Util.inherits(ChangeUser, Sequence); +Util.inherits(ChangeUser, Handshake); function ChangeUser(options, callback) { - Sequence.call(this, options, callback); + Handshake.call(this, createHandshakeOptions(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.determinePacket = function determinePacket(firstByte) { @@ -24,14 +46,24 @@ ChangeUser.prototype.determinePacket = function determinePacket(firstByte) { }; 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 8fad0fcf3..1d9f1bf31 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,6 +37,18 @@ 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; }; @@ -87,6 +106,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, @@ -94,9 +123,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 })); }; @@ -119,6 +146,70 @@ 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) { + // The connection is encrypted or a local socket, so the password can be sent in plaintext + this.emit('packet', new Packets.ClearTextPasswordPacket({ + data: password + })); + return; + } + + var secureAuth = this._config.secureAuth; + + if (secureAuth) { + if (secureAuth === true || secureAuth.key === undefined) { + // Fetch the authentication RSA public key from the server + this._waitingForServerPublicKey = true; + this.emit('packet', new Packets.HandshakeResponse41Packet()); + return; + } + + if (typeof secureAuth.key === 'string') { + // Use the provided authentication RSA public key + 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); +}; + +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); + } +}; + 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 f7a367518..74c734e47 100644 --- a/test/FakeServer.js +++ b/test/FakeServer.js @@ -67,6 +67,7 @@ function FakeConnection(socket) { this._expectedNextPacket = null; this._handshakeInitializationPacket = null; this._handshakeOptions = {}; + this._sentPublicKey = false; socket.on('data', this._handleData.bind(this)); } @@ -91,6 +92,11 @@ FakeConnection.prototype.error = function deny(message, errno) { 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 + ? true + : this._handshakeOptions.secureAuth; var packetOptions = common.extend({ scrambleBuff1 : Buffer.from('1020304050607080', 'hex'), @@ -109,19 +115,43 @@ FakeConnection.prototype.ok = function ok() { this._parser.resetPacketNumber(); }; +function formatExpectedError(got, expected) { + return 'expected ' + expected.toString('hex') + ' got ' + got.toString('hex'); +} + FakeConnection.prototype._sendAuthResponse = function _sendAuthResponse(got, expected) { if (expected.toString('hex') === got.toString('hex')) { this.ok(); } else { - this.deny('expected ' + expected.toString('hex') + ' got ' + got.toString('hex')); + this.deny(formatExpectedError(got, expected)); } this._parser.resetPacketNumber(); }; +FakeConnection.prototype._sendEncryptedAuthResponse = function _sendEncryptedAuthResponse(got, expected) { + if (expected.length === got.length) { + this._sendPacket(new Packets.OkPacket()); + } else { + this.deny(formatExpectedError(got, expected)); + } +}; + +FakeConnection.prototype._resetAuthProcess = function _resetAuthProcess(got, expected) { + if (expected.toString('hex') === got.toString('hex')) { + this._sendPacket(new Packets.PerformFullAuthenticationPacket()); + } else { + this.deny(formatExpectedError(got, expected)); + } +}; + FakeConnection.prototype._sendPacket = function(packet) { switch (packet.constructor) { + case Packets.AuthMoreDataPacket: + this._sentPublicKey = true; + // Fall through case Packets.AuthSwitchRequestPacket: + case Packets.PerformFullAuthenticationPacket: this._expectedNextPacket = Packets.AuthSwitchResponsePacket; break; case Packets.HandshakeInitializationPacket: @@ -331,6 +361,11 @@ FakeConnection.prototype._parsePacket = function() { this._socket.end(); } break; + case Packets.HandshakeResponse41Packet: + this._sendPacket(new Packets.AuthMoreDataPacket({ + data: common.getServerPublicKey() + })); + break; default: if (!this.emit(packet.constructor.name, packet)) { throw new Error('Unexpected packet: ' + Packet.name); @@ -348,6 +383,15 @@ FakeConnection.prototype._determinePacket = function _determinePacket() { : Packets.ClientAuthenticationPacket; } + if (Packet === Packets.AuthSwitchResponsePacket) { + var secureAuth = this._handshakeOptions.secureAuth; + var shouldSendPublicKey = !this._sentPublicKey && secureAuth && + (secureAuth === true || secureAuth.key === undefined); + return shouldSendPublicKey + ? Packets.HandshakeResponse41Packet + : Packets.AuthSwitchResponsePacket; + } + this._expectedNextPacket = null; return Packet; diff --git a/test/common.js b/test/common.js index f0eccbe08..94271de3c 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'); @@ -32,6 +33,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'); @@ -147,6 +151,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-caching-sha2-password-fast.js b/test/unit/connection/test-auth-caching-sha2-password-fast.js new file mode 100644 index 000000000..1c6f7f67f --- /dev/null +++ b/test/unit/connection/test-auth-caching-sha2-password-fast.js @@ -0,0 +1,40 @@ +var assert = require('assert'); +var common = require('../../common'); +var Packets = require(common.lib + '/protocol/packets'); + +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.on('clientAuthentication', function () { + this._sendPacket(new Packets.FastAuthSuccessPacket()); + this._sendPacket(new Packets.OkPacket()); + }); + + incomingConnection.handshake({ + authMethodName : 'caching_sha2_password', + serverCapabilities1 : common.ClientConstants.CLIENT_SSL + }); +}); + +process.on('exit', function() { + assert.equal(connected.fieldCount, 0); +}); diff --git a/test/unit/connection/test-auth-caching-sha2-password-full-error.js b/test/unit/connection/test-auth-caching-sha2-password-full-error.js new file mode 100644 index 000000000..1e47db32f --- /dev/null +++ b/test/unit/connection/test-auth-caching-sha2-password-full-error.js @@ -0,0 +1,42 @@ +var assert = require('assert'); +var crypto = require('crypto'); +var common = require('../../common'); +var Auth = require(common.lib + '/protocol/Auth'); + +var random = crypto.pseudoRandomBytes || crypto.randomBytes; // Depends on node.js version + +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, 'should get error'); + assert.equal(err.code, 'HANDSHAKE_SECURE_TRANSPORT_REQUIRED'); + assert.ok(err.fatal); + + connection.destroy(); + server.destroy(); + }); +}); + +server.on('connection', function(incomingConnection) { + random(20, function (err, scramble) { + assert.ifError(err); + + incomingConnection.on('clientAuthentication', function (packet) { + this._resetAuthProcess(packet.scrambleBuff, Auth.sha2Token('passwd', scramble)); + }); + + incomingConnection.handshake({ + authMethodName : 'caching_sha2_password', + scrambleBuff1 : scramble.slice(0, 8), + scrambleBuff2 : scramble.slice(8, 20) + }); + }); +}); diff --git a/test/unit/connection/test-auth-caching-sha2-password-full-key-error.js b/test/unit/connection/test-auth-caching-sha2-password-full-key-error.js new file mode 100644 index 000000000..c24d9914d --- /dev/null +++ b/test/unit/connection/test-auth-caching-sha2-password-full-key-error.js @@ -0,0 +1,45 @@ +var assert = require('assert'); +var crypto = require('crypto'); +var common = require('../../common'); +var Auth = require(common.lib + '/protocol/Auth'); + +var random = crypto.pseudoRandomBytes || crypto.randomBytes; // Depends on node.js version + +var connection = common.createConnection({ + port : common.fakeServerPort, + password : 'passwd', + secureAuth : { + key : false, + 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, 'should get error'); + assert.equal(err.code, 'HANDSHAKE_SECURE_TRANSPORT_REQUIRED'); + assert.ok(err.fatal); + + connection.destroy(); + server.destroy(); + }); +}); + +server.on('connection', function(incomingConnection) { + random(20, function (err, scramble) { + assert.ifError(err); + + incomingConnection.on('clientAuthentication', function (packet) { + this._resetAuthProcess(packet.scrambleBuff, Auth.sha2Token('passwd', scramble)); + }); + + incomingConnection.handshake({ + authMethodName : 'caching_sha2_password', + scrambleBuff1 : scramble.slice(0, 8), + scrambleBuff2 : scramble.slice(8, 20) + }); + }); +}); diff --git a/test/unit/connection/test-auth-caching-sha2-password-full-key-no-padding.js b/test/unit/connection/test-auth-caching-sha2-password-full-key-no-padding.js new file mode 100644 index 000000000..21610d39f --- /dev/null +++ b/test/unit/connection/test-auth-caching-sha2-password-full-key-no-padding.js @@ -0,0 +1,61 @@ +var assert = require('assert'); +var crypto = require('crypto'); +var common = require('../../common'); +var Auth = require(common.lib + '/protocol/Auth'); +var Buffer = require('safe-buffer').Buffer; + +crypto.publicEncrypt = crypto.publicEncrypt || function () { // Shim for Node < 0.11 + return Buffer.from('passwd', 'utf8'); +}; + +var random = crypto.pseudoRandomBytes || crypto.randomBytes; // Depends on node.js version + +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(); + }); +}); + +server.on('connection', function(incomingConnection) { + random(20, function (err, scramble) { + assert.ifError(err); + + incomingConnection.on('clientAuthentication', function (packet) { + this._resetAuthProcess(packet.scrambleBuff, Auth.sha2Token('passwd', scramble)); + }); + + incomingConnection.on('authSwitchResponse', function (packet) { + var expected = Auth.encrypt('passwd', scramble, secureAuth); + this._sendEncryptedAuthResponse(packet.data, expected); + }); + + incomingConnection.handshake({ + authMethodName : 'caching_sha2_password', + secureAuth : secureAuth, + scrambleBuff1 : scramble.slice(0, 8), + scrambleBuff2 : scramble.slice(8, 20) + }); + }); +}); + +process.on('exit', function() { + assert.equal(connected.fieldCount, 0); +}); diff --git a/test/unit/connection/test-auth-caching-sha2-password-full-key-padding.js b/test/unit/connection/test-auth-caching-sha2-password-full-key-padding.js new file mode 100644 index 000000000..3e4843882 --- /dev/null +++ b/test/unit/connection/test-auth-caching-sha2-password-full-key-padding.js @@ -0,0 +1,62 @@ +var assert = require('assert'); +var crypto = require('crypto'); +var common = require('../../common'); +var Auth = require(common.lib + '/protocol/Auth'); +var Buffer = require('safe-buffer').Buffer; + +crypto.publicEncrypt = crypto.publicEncrypt || function () { // Shim for Node < 0.11 + return Buffer.from('passwd', 'utf8'); +}; + +var random = crypto.pseudoRandomBytes || crypto.randomBytes; // Depends on node.js version + +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(); + }); +}); + +server.on('connection', function(incomingConnection) { + random(20, function (err, scramble) { + assert.ifError(err); + + incomingConnection.on('clientAuthentication', function (packet) { + this._resetAuthProcess(packet.scrambleBuff, Auth.sha2Token('passwd', scramble)); + }); + + incomingConnection.on('authSwitchResponse', function (packet) { + var expected = Auth.encrypt('passwd', scramble, secureAuth); + this._sendEncryptedAuthResponse(packet.data, expected); + }); + + incomingConnection.handshake({ + authMethodName : 'caching_sha2_password', + secureAuth : secureAuth, + scrambleBuff1 : scramble.slice(0, 8), + scrambleBuff2 : scramble.slice(8, 20) + }); + }); +}); + +process.on('exit', function() { + assert.equal(connected.fieldCount, 0); +}); diff --git a/test/unit/connection/test-auth-caching-sha2-password-full-padding-no-key.js b/test/unit/connection/test-auth-caching-sha2-password-full-padding-no-key.js new file mode 100644 index 000000000..609b12635 --- /dev/null +++ b/test/unit/connection/test-auth-caching-sha2-password-full-padding-no-key.js @@ -0,0 +1,59 @@ +var assert = require('assert'); +var crypto = require('crypto'); +var common = require('../../common'); +var Auth = require(common.lib + '/protocol/Auth'); +var Buffer = require('safe-buffer').Buffer; + +crypto.publicEncrypt = crypto.publicEncrypt || function () { // Shim for Node < 0.11 + return Buffer.from('passwd', 'utf8'); +}; + +var random = crypto.pseudoRandomBytes || crypto.randomBytes; // Depends on node.js version + +var connection = common.createConnection({ + port : common.fakeServerPort, + password : 'passwd', + secureAuth : { + padding: common.PlatformConstants.RSA_PKCS1_OAEP_PADDING + } +}); +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) { + random(20, function (err, scramble) { + assert.ifError(err); + + incomingConnection.on('clientAuthentication', function (packet) { + this._resetAuthProcess(packet.scrambleBuff, Auth.sha2Token('passwd', scramble)); + }); + + incomingConnection.on('authSwitchResponse', function (packet) { + var expected = Auth.encrypt('passwd', scramble, {key: common.getServerPublicKey()}); + this._sendEncryptedAuthResponse(packet.data, expected); + }); + + incomingConnection.handshake({ + authMethodName : 'caching_sha2_password', + scrambleBuff1 : scramble.slice(0, 8), + scrambleBuff2 : scramble.slice(8, 20) + }); + }); +}); + +process.on('exit', function() { + assert.equal(connected.fieldCount, 0); +}); diff --git a/test/unit/connection/test-auth-caching-sha2-password-full-ssl.js b/test/unit/connection/test-auth-caching-sha2-password-full-ssl.js new file mode 100644 index 000000000..6bd93b77c --- /dev/null +++ b/test/unit/connection/test-auth-caching-sha2-password-full-ssl.js @@ -0,0 +1,60 @@ +var assert = require('assert'); +var crypto = require('crypto'); +var common = require('../../common'); +var Auth = require(common.lib + '/protocol/Auth'); +var Buffer = require('safe-buffer').Buffer; + +crypto.publicEncrypt = crypto.publicEncrypt || function () { // Shim for Node < 0.11 + return Buffer.from('passwd', 'utf8'); +}; + +var random = crypto.pseudoRandomBytes || crypto.randomBytes; // Depends on node.js version + +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) { + random(20, function (err, scramble) { + assert.ifError(err); + + incomingConnection.on('clientAuthentication', function (packet) { + this._resetAuthProcess(packet.scrambleBuff, Auth.sha2Token('passwd', scramble)); + }); + + incomingConnection.on('authSwitchResponse', function (packet) { + // Password should be sent as plain text + this._sendAuthResponse(packet.data, Buffer.from('passwd\0', 'utf8')); + }); + + incomingConnection.handshake({ + authMethodName : 'caching_sha2_password', + secureAuth : false, + serverCapabilities1 : common.ClientConstants.CLIENT_SSL, + scrambleBuff1 : scramble.slice(0, 8), + scrambleBuff2 : scramble.slice(8, 20) + }); + }); +}); + +process.on('exit', function() { + assert.equal(connected.fieldCount, 0); +}); diff --git a/test/unit/connection/test-auth-caching-sha2-password-full.js b/test/unit/connection/test-auth-caching-sha2-password-full.js new file mode 100644 index 000000000..78f41adf5 --- /dev/null +++ b/test/unit/connection/test-auth-caching-sha2-password-full.js @@ -0,0 +1,56 @@ +var assert = require('assert'); +var crypto = require('crypto'); +var common = require('../../common'); +var Auth = require(common.lib + '/protocol/Auth'); +var Buffer = require('safe-buffer').Buffer; + +crypto.publicEncrypt = crypto.publicEncrypt || function () { // Shim for Node < 0.11 + return Buffer.from('passwd', 'utf8'); +}; + +var random = crypto.pseudoRandomBytes || crypto.randomBytes; // Depends on node.js version + +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) { + random(20, function (err, scramble) { + assert.ifError(err); + + incomingConnection.on('clientAuthentication', function (packet) { + this._resetAuthProcess(packet.scrambleBuff, Auth.sha2Token('passwd', scramble)); + }); + + incomingConnection.on('authSwitchResponse', function (packet) { + var expected = Auth.encrypt('passwd', scramble, {key: common.getServerPublicKey()}); + this._sendEncryptedAuthResponse(packet.data, expected); + }); + + incomingConnection.handshake({ + authMethodName : 'caching_sha2_password', + scrambleBuff1 : scramble.slice(0, 8), + scrambleBuff2 : scramble.slice(8, 20) + }); + }); +}); + +process.on('exit', function() { + assert.equal(connected.fieldCount, 0); +}); diff --git a/test/unit/connection/test-auth-caching-sha2-password-public-key-encryption-error.js b/test/unit/connection/test-auth-caching-sha2-password-public-key-encryption-error.js new file mode 100644 index 000000000..b60cc0891 --- /dev/null +++ b/test/unit/connection/test-auth-caching-sha2-password-public-key-encryption-error.js @@ -0,0 +1,46 @@ +var assert = require('assert'); +var crypto = require('crypto'); +var common = require('../../common'); +var Auth = require(common.lib + '/protocol/Auth'); + +var publicEncrypt = crypto.publicEncrypt; +crypto.publicEncrypt = undefined; // Test for when publicEncrypt is unavailable + +var random = crypto.pseudoRandomBytes || crypto.randomBytes; // Depends on node.js version + +var connection = common.createConnection({ + port : common.fakeServerPort, + password : 'passwd' +}); +var server = common.createFakeServer(); + +server.listen(common.fakeServerPort, function(err) { + assert.ifError(err); + + connection.connect(function (err) { + assert.ok(err, 'should get 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) { + random(20, function (err, scramble) { + assert.ifError(err); + + incomingConnection.on('clientAuthentication', function (packet) { + this._resetAuthProcess(packet.scrambleBuff, Auth.sha2Token('passwd', scramble)); + }); + + incomingConnection.handshake({ + authMethodName : 'caching_sha2_password', + scrambleBuff1 : scramble.slice(0, 8), + scrambleBuff2 : scramble.slice(8, 20) + }); + }); +}); diff --git a/test/unit/connection/test-auth-password.js b/test/unit/connection/test-auth-native-password.js similarity index 87% rename from test/unit/connection/test-auth-password.js rename to test/unit/connection/test-auth-native-password.js index 06f2aa48b..943e4940f 100644 --- a/test/unit/connection/test-auth-password.js +++ b/test/unit/connection/test-auth-native-password.js @@ -29,8 +29,9 @@ server.on('connection', function(incomingConnection) { }); incomingConnection.handshake({ - scrambleBuff1 : scramble.slice(0, 8), - scrambleBuff2 : scramble.slice(8, 20) + authMethodName : 'mysql_native_password', + scrambleBuff1 : scramble.slice(0, 8), + scrambleBuff2 : scramble.slice(8, 20) }); }); }); 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..06f40c285 --- /dev/null +++ b/test/unit/connection/test-auth-switch-caching-sha2.js @@ -0,0 +1,48 @@ +var assert = require('assert'); +var crypto = require('crypto'); +var common = require('../../common'); + +var random = crypto.pseudoRandomBytes || crypto.randomBytes; // Depends on node.js version + +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) { + random(20, function (err, scramble) { + assert.ifError(err); + + incomingConnection.on('authSwitchResponse', function (packet) { + this._sendAuthResponse(packet.data, common.Auth.sha2Token('authswitch', scramble)); + }); + + incomingConnection.on('clientAuthentication', function () { + this.authSwitchRequest({ + authMethodName : 'caching_sha2_password', + authMethodData : scramble + }); + }); + + incomingConnection.handshake(); + }); +}); + +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..4fdcbc466 --- /dev/null +++ b/test/unit/connection/test-auth-switch-old.js @@ -0,0 +1,48 @@ +var assert = require('assert'); +var crypto = require('crypto'); +var common = require('../../common'); + +var random = crypto.pseudoRandomBytes || crypto.randomBytes; // Depends on node.js version + +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) { + random(20, function (err, scramble) { + assert.ifError(err); + + incomingConnection.on('authSwitchResponse', function (packet) { + this._sendAuthResponse(packet.data, common.Auth.token('authswitch', scramble)); + }); + + incomingConnection.on('clientAuthentication', function () { + this.authSwitchRequest({ + authMethodName : 'mysql_old_password', + authMethodData : scramble + }); + }); + + incomingConnection.handshake(); + }); +}); + +process.on('exit', function() { + assert.equal(connected.fieldCount, 0); +}); 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..260be6105 --- /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..23ef9059b --- /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' + }); +}); From 1044a14745b97a43a5887d3c586b1d3a022d5154 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 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 4f197d769..dd2429a16 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,6 +29,8 @@ matrix: env: "DOCKER_MYSQL_TYPE=mysql DOCKER_MYSQL_VERSION=5.5" - node_js: *lts env: "DOCKER_MYSQL_TYPE=mysql DOCKER_MYSQL_VERSION=5.6" + - node_js: *lts + env: "DOCKER_MYSQL_TYPE=mysql DOCKER_MYSQL_VERSION=8.0" - node_js: *lts env: "DOCKER_MYSQL_TYPE=mariadb DOCKER_MYSQL_VERSION=5.5" - node_js: *lts