Skip to content

Commit 111a283

Browse files
ruiquelhasnwoltman
authored andcommitted
Add support for caching_sha2_password handshake
1 parent 08fe203 commit 111a283

31 files changed

+1039
-29
lines changed

Readme.md

+81-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- [Community](#community)
1717
- [Establishing connections](#establishing-connections)
1818
- [Connection options](#connection-options)
19+
- [Authentication options](#authentication-options)
1920
- [SSL options](#ssl-options)
2021
- [Terminating connections](#terminating-connections)
2122
- [Pooling connections](#pooling-connections)
@@ -235,6 +236,7 @@ issue [#501](https://github.com/mysqljs/mysql/issues/501). (Default: `false`)
235236
also possible to blacklist default ones. For more information, check
236237
[Connection Flags](#connection-flags).
237238
* `ssl`: object with ssl parameters or a string containing name of ssl profile. See [SSL options](#ssl-options).
239+
* `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).
238240

239241

240242
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
247249
Note: The query values are first attempted to be parsed as JSON, and if that
248250
fails assumed to be plaintext strings.
249251

252+
### Authentication options
253+
254+
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).
255+
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.
256+
257+
The initial handshake for this plugin will only work if the connection is secure or the server
258+
uses a valid RSA public key for the given type of authentication (both default MySQL 8 settings).
259+
By default, if the connection is not secure, the client will fetch the public key from the server
260+
and use it (alongside a server-generated nonce) to encrypt the password.
261+
262+
After a successful initial handshake, any subsequent handshakes will always work, until the
263+
server shuts down or the password is somehow removed from the server authentication cache.
264+
265+
The default connection options provide compatibility with both MySQL 5.7 and MySQL 8 servers.
266+
267+
```js
268+
// default options
269+
var connection = mysql.createConnection({
270+
ssl : false,
271+
secureAuth : true
272+
});
273+
```
274+
275+
If you are in control of the server public key, you can also provide it explicitly and avoid
276+
the additional round-trip.
277+
278+
```js
279+
var connection = mysql.createConnection({
280+
ssl : false,
281+
secureAuth : {
282+
key: fs.readFileSync(__dirname + '/mysql-pub.key')
283+
}
284+
});
285+
```
286+
287+
As an alternative to providing just the key, you can provide additional options, in the same
288+
format as [crypto.publicEncrypt](https://nodejs.org/docs/latest-v4.x/api/crypto.html#crypto_crypto_publicencrypt_public_key_buffer),
289+
which means you can also specify the key padding type.
290+
291+
**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).
292+
293+
```js
294+
var constants = require('constants');
295+
296+
var connection = mysql.createConnection({
297+
ssl : false,
298+
secureAuth : {
299+
key: fs.readFileSync(__dirname + '/mysql-pub.key'),
300+
padding: constants.RSA_PKCS1_PADDING
301+
}
302+
});
303+
```
304+
305+
At least one of these options needs to be enabled for the initial handshake to work. So, the
306+
following flavour will also work.
307+
308+
```js
309+
var connection = mysql.createConnection({
310+
ssl : true, // or a valid ssl configuration object
311+
secureAuth : false
312+
});
313+
```
314+
315+
If both `secureAuth` and `ssl` options are disabled, the connection will fail.
316+
317+
```js
318+
var connection = mysql.createConnection({
319+
ssl : false,
320+
secureAuth : false
321+
});
322+
323+
connection.connect(function (err) {
324+
console.log(err.message); // 'Authentication requires secure connection'
325+
});
326+
```
327+
250328
### SSL options
251329

252330
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:
560638
* `password`: The password of the new user (defaults to the previous one).
561639
* `charset`: The new charset (defaults to the previous one).
562640
* `database`: The new database (defaults to the previous one).
641+
* `timeout`: An optional [timeout](#timeouts).
563642

564643
A sometimes useful side effect of this functionality is that this function also
565644
resets any connection state (variables, transactions, etc.).
@@ -611,7 +690,7 @@ connection.query('SELECT * FROM `books` WHERE `author` = ?', ['David'], function
611690
The third form `.query(options, callback)` comes when using various advanced
612691
options on the query, like [escaping query values](#escaping-query-values),
613692
[joins with overlapping column names](#joins-with-overlapping-column-names),
614-
[timeouts](#timeout), and [type casting](#type-casting).
693+
[timeouts](#timeouts), and [type casting](#type-casting).
615694

616695
```js
617696
connection.query({
@@ -1393,6 +1472,7 @@ The following flags are sent by default on a new connection:
13931472
- `LONG_PASSWORD` - Use the improved version of Old Password Authentication.
13941473
- `MULTI_RESULTS` - Can handle multiple resultsets for COM_QUERY.
13951474
- `ODBC` Old; no effect.
1475+
- `PLUGIN_AUTH` - Support different authentication plugins.
13961476
- `PROTOCOL_41` - Uses the 4.1 protocol.
13971477
- `PS_MULTI_RESULTS` - Can handle multiple resultsets for COM_STMT_EXECUTE.
13981478
- `RESERVED` - Old flag for the 4.1 protocol.

lib/ConnectionConfig.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ function ConnectionConfig(options) {
5858
// Set the client flags
5959
var defaultFlags = ConnectionConfig.getDefaultFlags(options);
6060
this.clientFlags = ConnectionConfig.mergeFlags(defaultFlags, options.flags);
61+
62+
this.secureAuth = options.secureAuth !== undefined ? options.secureAuth : true;
6163
}
6264

6365
ConnectionConfig.mergeFlags = function mergeFlags(defaultFlags, userFlags) {
@@ -106,7 +108,7 @@ ConnectionConfig.getDefaultFlags = function getDefaultFlags(options) {
106108
'+LONG_PASSWORD', // Use the improved version of Old Password Authentication
107109
'+MULTI_RESULTS', // Can handle multiple resultsets for COM_QUERY
108110
'+ODBC', // Special handling of ODBC behaviour
109-
'-PLUGIN_AUTH', // Does *NOT* support auth plugins
111+
'+PLUGIN_AUTH', // Supports auth plugins
110112
'+PROTOCOL_41', // Uses the 4.1 protocol
111113
'+PS_MULTI_RESULTS', // Can handle multiple resultsets for COM_STMT_EXECUTE
112114
'+RESERVED', // Unused

lib/protocol/Auth.js

+42-3
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,38 @@ var Auth = exports;
44

55
function auth(name, data, options) {
66
options = options || {};
7+
var scramble = data.slice(0, 20);
78

89
switch (name) {
10+
case 'caching_sha2_password':
11+
return Auth.sha2Token(options.password, scramble);
912
case 'mysql_native_password':
10-
return Auth.token(options.password, data.slice(0, 20));
13+
return Auth.token(options.password, scramble);
14+
case 'mysql_old_password':
15+
return Auth.scramble323(scramble, options.password);
1116
default:
1217
return undefined;
1318
}
1419
}
1520
Auth.auth = auth;
1621

17-
function sha1(msg) {
18-
var hash = Crypto.createHash('sha1');
22+
function createHash(msg, algorithm) {
23+
algorithm = algorithm || 'sha1';
24+
var hash = Crypto.createHash(algorithm);
1925
hash.update(msg, 'binary');
2026
return hash.digest('binary');
2127
}
28+
29+
function sha1(msg) {
30+
return createHash(msg, 'sha1');
31+
}
32+
33+
function sha256(msg) {
34+
return createHash(msg, 'sha256');
35+
}
36+
2237
Auth.sha1 = sha1;
38+
Auth.sha256 = sha256;
2339

2440
function xor(a, b) {
2541
a = Buffer.from(a, 'binary');
@@ -44,6 +60,29 @@ Auth.token = function(password, scramble) {
4460
return xor(stage3, stage1);
4561
};
4662

63+
Auth.sha2Token = function(password, scramble) {
64+
if (!password) {
65+
return Buffer.alloc(0);
66+
}
67+
68+
// password must be in binary format, not utf8
69+
var stage1 = sha256((Buffer.from(password, 'utf8')).toString('binary'));
70+
var stage2 = sha256(stage1);
71+
var stage3 = sha256(stage2 + scramble.toString('binary'));
72+
return xor(stage1, stage3);
73+
};
74+
75+
Auth.encrypt = function(password, scramble, key) {
76+
if (typeof Crypto.publicEncrypt !== 'function') {
77+
var err = new Error('The Node.js version does not support public key encryption');
78+
err.code = 'PUB_KEY_ENCRYPTION_NOT_AVAILABLE';
79+
throw err;
80+
}
81+
82+
var stage1 = xor((Buffer.from(password + '\0', 'utf8')).toString('binary'), scramble.toString('binary'));
83+
return Crypto.publicEncrypt(key, stage1);
84+
};
85+
4786
// This is a port of sql/password.c:hash_password which needs to be used for
4887
// pre-4.1 passwords.
4988
Auth.hashPassword = function(password) {
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module.exports = AuthMoreDataPacket;
2+
function AuthMoreDataPacket(options) {
3+
options = options || {};
4+
5+
this.status = 0x01;
6+
this.data = options.data;
7+
}
8+
9+
AuthMoreDataPacket.prototype.parse = function parse(parser) {
10+
this.status = parser.parseUnsignedNumber(1);
11+
this.data = parser.parsePacketTerminatedString();
12+
};
13+
14+
AuthMoreDataPacket.prototype.write = function parse(writer) {
15+
writer.writeUnsignedNumber(this.status);
16+
writer.writeString(this.data);
17+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module.exports = ClearTextPasswordPacket;
2+
function ClearTextPasswordPacket(options) {
3+
this.data = options.data;
4+
}
5+
6+
ClearTextPasswordPacket.prototype.write = function write(writer) {
7+
writer.writeNullTerminatedString(this.data);
8+
};

lib/protocol/packets/ComChangeUserPacket.js

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ function ComChangeUserPacket(options) {
77
this.scrambleBuff = options.scrambleBuff;
88
this.database = options.database;
99
this.charsetNumber = options.charsetNumber;
10+
this.authPlugin = options.authPlugin;
1011
}
1112

1213
ComChangeUserPacket.prototype.parse = function(parser) {
@@ -15,6 +16,7 @@ ComChangeUserPacket.prototype.parse = function(parser) {
1516
this.scrambleBuff = parser.parseLengthCodedBuffer();
1617
this.database = parser.parseNullTerminatedString();
1718
this.charsetNumber = parser.parseUnsignedNumber(1);
19+
this.authPlugin = parser.parseNullTerminatedString();
1820
};
1921

2022
ComChangeUserPacket.prototype.write = function(writer) {
@@ -23,4 +25,5 @@ ComChangeUserPacket.prototype.write = function(writer) {
2325
writer.writeLengthCodedBuffer(this.scrambleBuff);
2426
writer.writeNullTerminatedString(this.database);
2527
writer.writeUnsignedNumber(2, this.charsetNumber);
28+
writer.writeNullTerminatedString(this.authPlugin);
2629
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module.exports = FastAuthSuccessPacket;
2+
function FastAuthSuccessPacket() {
3+
this.status = 0x01;
4+
this.authMethodName = 0x03;
5+
}
6+
7+
FastAuthSuccessPacket.prototype.parse = function parse(parser) {
8+
this.status = parser.parseUnsignedNumber(1);
9+
this.authMethodName = parser.parseUnsignedNumber(1);
10+
};
11+
12+
FastAuthSuccessPacket.prototype.write = function write(writer) {
13+
writer.writeUnsignedNumber(1, this.status);
14+
writer.writeUnsignedNumber(1, this.authMethodName);
15+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module.exports = HandshakeResponse41Packet;
2+
function HandshakeResponse41Packet() {
3+
this.status = 0x02;
4+
}
5+
6+
HandshakeResponse41Packet.prototype.parse = function write(parser) {
7+
this.status = parser.parseUnsignedNumber(1);
8+
};
9+
10+
HandshakeResponse41Packet.prototype.write = function write(writer) {
11+
writer.writeUnsignedNumber(1, this.status);
12+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module.exports = PerformFullAuthenticationPacket;
2+
function PerformFullAuthenticationPacket() {
3+
this.status = 0x01;
4+
this.authMethodName = 0x04;
5+
}
6+
7+
PerformFullAuthenticationPacket.prototype.parse = function parse(parser) {
8+
this.status = parser.parseUnsignedNumber(1);
9+
this.authMethodName = parser.parseUnsignedNumber(1);
10+
};
11+
12+
PerformFullAuthenticationPacket.prototype.write = function write(writer) {
13+
writer.writeUnsignedNumber(1, this.status);
14+
writer.writeUnsignedNumber(1, this.authMethodName);
15+
};

lib/protocol/packets/index.js

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
exports.AuthMoreDataPacket = require('./AuthMoreDataPacket');
12
exports.AuthSwitchRequestPacket = require('./AuthSwitchRequestPacket');
23
exports.AuthSwitchResponsePacket = require('./AuthSwitchResponsePacket');
4+
exports.ClearTextPasswordPacket = require('./ClearTextPasswordPacket');
35
exports.ClientAuthenticationPacket = require('./ClientAuthenticationPacket');
46
exports.ComChangeUserPacket = require('./ComChangeUserPacket');
57
exports.ComPingPacket = require('./ComPingPacket');
@@ -9,12 +11,15 @@ exports.ComStatisticsPacket = require('./ComStatisticsPacket');
911
exports.EmptyPacket = require('./EmptyPacket');
1012
exports.EofPacket = require('./EofPacket');
1113
exports.ErrorPacket = require('./ErrorPacket');
14+
exports.FastAuthSuccessPacket = require('./FastAuthSuccessPacket');
1215
exports.Field = require('./Field');
1316
exports.FieldPacket = require('./FieldPacket');
1417
exports.HandshakeInitializationPacket = require('./HandshakeInitializationPacket');
18+
exports.HandshakeResponse41Packet = require('./HandshakeResponse41Packet');
1519
exports.LocalDataFilePacket = require('./LocalDataFilePacket');
1620
exports.OkPacket = require('./OkPacket');
1721
exports.OldPasswordPacket = require('./OldPasswordPacket');
22+
exports.PerformFullAuthenticationPacket = require('./PerformFullAuthenticationPacket');
1823
exports.ResultSetHeaderPacket = require('./ResultSetHeaderPacket');
1924
exports.RowDataPacket = require('./RowDataPacket');
2025
exports.SSLRequestPacket = require('./SSLRequestPacket');

0 commit comments

Comments
 (0)