Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix MySQL 8.0.x incompatibilities #1962

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions .travis.yml
Expand Up @@ -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"
Expand Down
87 changes: 87 additions & 0 deletions Readme.md
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -1371,12 +1449,21 @@ 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.
- `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`:

Expand Down
4 changes: 3 additions & 1 deletion lib/ConnectionConfig.js
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
38 changes: 36 additions & 2 deletions lib/protocol/Auth.js
Expand Up @@ -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');
Expand All @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions lib/protocol/Parser.js
Expand Up @@ -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) {
Expand Down
17 changes: 17 additions & 0 deletions 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);
};
8 changes: 8 additions & 0 deletions 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);
};
3 changes: 3 additions & 0 deletions lib/protocol/packets/ComChangeUserPacket.js
Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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);
};
15 changes: 15 additions & 0 deletions 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);
};
12 changes: 12 additions & 0 deletions 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);
};
15 changes: 15 additions & 0 deletions 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);
};
5 changes: 5 additions & 0 deletions 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');
Expand All @@ -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');
Expand Down
39 changes: 25 additions & 14 deletions 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;
Expand Down