Skip to content

Commit

Permalink
Add support for caching_sha2_password handshake
Browse files Browse the repository at this point in the history
  • Loading branch information
ruiquelhas authored and nwoltman committed Jun 12, 2019
1 parent 08fe203 commit 111a283
Show file tree
Hide file tree
Showing 31 changed files with 1,039 additions and 29 deletions.
82 changes: 81 additions & 1 deletion Readme.md
Original file line number Diff line number Diff line change
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 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,
Expand Down Expand Up @@ -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.).
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion lib/ConnectionConfig.js
Original file line number Diff line number Diff line change
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
45 changes: 42 additions & 3 deletions lib/protocol/Auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,38 @@ var Auth = exports;

function auth(name, data, options) {
options = options || {};
var scramble = data.slice(0, 20);

switch (name) {
case 'caching_sha2_password':
return Auth.sha2Token(options.password, scramble);
case 'mysql_native_password':
return Auth.token(options.password, data.slice(0, 20));
return Auth.token(options.password, scramble);
case 'mysql_old_password':
return Auth.scramble323(scramble, 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');
Expand All @@ -44,6 +60,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
17 changes: 17 additions & 0 deletions lib/protocol/packets/AuthMoreDataPacket.js
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 111a283

Please sign in to comment.