Skip to content

Commit 85a9687

Browse files
authored
Merge pull request #345 from Drake103/caching-sha2-password-support
Implement caching_sha2_password support.
2 parents d174357 + 6a5ff23 commit 85a9687

File tree

10 files changed

+271
-72
lines changed

10 files changed

+271
-72
lines changed

.ci/docker-run.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ for i in `seq 1 30`; do
4949
if [ $? -ne 0 ]; then continue; fi
5050
fi
5151

52+
if [[ $FEATURES == *"CachingSha2Password"* ]]; then
53+
docker exec -it $NAME bash -c 'mysql -uroot -ptest < /etc/mysql/conf.d/init_caching_sha2.sql' >/dev/null 2>&1
54+
if [ $? -ne 0 ]; then continue; fi
55+
fi
56+
5257
# exit if successful
5358
docker exec -it $NAME mysql -ussltest -ptest \
5459
--ssl-mode=REQUIRED \

.ci/server/init_caching_sha2.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
CREATE USER 'caching-sha2-user'@'%' IDENTIFIED WITH caching_sha2_password BY 'Cach!ng-Sh@2-Pa55';
2+
GRANT ALL PRIVILEGES ON *.* TO 'caching-sha2-user'@'%';

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ env:
1111
FEATURES=Json,StoredProcedures,Sha256Password,LargePackets
1212
- IMAGE=mysql:8.0
1313
NAME=mysql
14-
FEATURES=Json,StoredProcedures,Sha256Password,LargePackets
14+
FEATURES=Json,StoredProcedures,Sha256Password,LargePackets,CachingSha2Password
1515
- IMAGE=percona:5.7
1616
NAME=percona
1717
FEATURES=Json,StoredProcedures,Sha256Password,OpenSsl,LargePackets

src/MySqlConnector/Serialization/AuthenticationUtility.cs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System;
1+
using System;
2+
using System.Linq;
23
using System.Security.Cryptography;
34
using System.Text;
45

@@ -39,5 +40,35 @@ public static byte[] HashPassword(byte[] challenge, int offset, string password)
3940
}
4041

4142
static readonly byte[] s_emptyArray = new byte[0];
43+
44+
public static byte[] CreateScrambleResponse(byte[] nonce, string password)
45+
{
46+
var scrambleResponse = string.IsNullOrEmpty(password)
47+
? s_emptyArray
48+
: HashPasswordWithNonce(nonce, password);
49+
50+
return scrambleResponse;
51+
}
52+
53+
private static byte[] HashPasswordWithNonce(byte[] nonce, string password)
54+
{
55+
using (var sha256 = SHA256.Create())
56+
{
57+
var passwordBytes = Encoding.UTF8.GetBytes(password);
58+
var hashedPassword = sha256.ComputeHash(passwordBytes);
59+
60+
var doubleHashedPassword = sha256.ComputeHash(hashedPassword);
61+
var combined = new byte[doubleHashedPassword.Length + nonce.Length];
62+
63+
Buffer.BlockCopy(doubleHashedPassword, 0, combined, 0, doubleHashedPassword.Length);
64+
Buffer.BlockCopy(nonce, 0, combined, doubleHashedPassword.Length, nonce.Length);
65+
66+
var xorBytes = sha256.ComputeHash(combined);
67+
for (int i = 0; i < hashedPassword.Length; i++)
68+
hashedPassword[i] ^= xorBytes[i];
69+
70+
return hashedPassword;
71+
}
72+
}
4273
}
4374
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
namespace MySql.Data.Serialization
2+
{
3+
internal class CachingSha2ServerResponsePayload
4+
{
5+
public const byte Signature = 0x01;
6+
7+
public const byte SuccessSignature = 0x03;
8+
9+
public const byte FullAuthRequiredSignature = 0x04;
10+
11+
private CachingSha2ServerResponsePayload(bool succeeded, bool fullAuthRequired)
12+
{
13+
Succeeded = succeeded;
14+
FullAuthRequired = fullAuthRequired;
15+
}
16+
17+
public bool Succeeded { get; }
18+
19+
public bool FullAuthRequired { get; }
20+
21+
public static CachingSha2ServerResponsePayload Create(PayloadData payload)
22+
{
23+
var reader = new ByteArrayReader(payload.ArraySegment);
24+
reader.ReadByte(Signature);
25+
var secondByte = reader.ReadByte();
26+
27+
return new CachingSha2ServerResponsePayload(
28+
secondByte == SuccessSignature,
29+
secondByte == FullAuthRequiredSignature);
30+
}
31+
}
32+
}

src/MySqlConnector/Serialization/MySqlSession.cs

Lines changed: 156 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ public async Task ConnectAsync(ConnectionSettings cs, IOBehavior ioBehavior, Can
216216
authPluginName = initialHandshake.AuthPluginName;
217217
else
218218
authPluginName = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.SecureConnection) == 0 ? "mysql_old_password" : "mysql_native_password";
219-
if (authPluginName != "mysql_native_password" && authPluginName != "sha256_password")
219+
if (authPluginName != "mysql_native_password" && authPluginName != "sha256_password" && authPluginName != "caching_sha2_password")
220220
throw new NotSupportedException("Authentication method '{0}' is not supported.".FormatInvariant(initialHandshake.AuthPluginName));
221221

222222
ServerVersion = new ServerVersion(Encoding.ASCII.GetString(initialHandshake.ServerVersion));
@@ -246,8 +246,7 @@ public async Task ConnectAsync(ConnectionSettings cs, IOBehavior ioBehavior, Can
246246
// if server doesn't support the authentication fast path, it will send a new challenge
247247
if (payload.HeaderByte == AuthenticationMethodSwitchRequestPayload.Signature)
248248
{
249-
await SwitchAuthenticationAsync(cs, payload, ioBehavior, cancellationToken).ConfigureAwait(false);
250-
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
249+
payload = await SwitchAuthenticationAsync(cs, payload, ioBehavior, cancellationToken).ConfigureAwait(false);
251250
}
252251

253252
OkPayload.Create(payload);
@@ -283,8 +282,7 @@ public async Task<bool> TryResetConnectionAsync(ConnectionSettings cs, IOBehavio
283282
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
284283
if (payload.HeaderByte == AuthenticationMethodSwitchRequestPayload.Signature)
285284
{
286-
await SwitchAuthenticationAsync(cs, payload, ioBehavior, cancellationToken).ConfigureAwait(false);
287-
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
285+
payload = await SwitchAuthenticationAsync(cs, payload, ioBehavior, cancellationToken).ConfigureAwait(false);
288286
}
289287
OkPayload.Create(payload);
290288
}
@@ -301,7 +299,7 @@ public async Task<bool> TryResetConnectionAsync(ConnectionSettings cs, IOBehavio
301299
return false;
302300
}
303301

304-
private async Task SwitchAuthenticationAsync(ConnectionSettings cs, PayloadData payload, IOBehavior ioBehavior, CancellationToken cancellationToken)
302+
private async Task<PayloadData> SwitchAuthenticationAsync(ConnectionSettings cs, PayloadData payload, IOBehavior ioBehavior, CancellationToken cancellationToken)
305303
{
306304
// if the server didn't support the hashed password; rehash with the new challenge
307305
var switchRequest = AuthenticationMethodSwitchRequestPayload.Create(payload);
@@ -312,82 +310,57 @@ private async Task SwitchAuthenticationAsync(ConnectionSettings cs, PayloadData
312310
var hashedPassword = AuthenticationUtility.CreateAuthenticationResponse(AuthPluginData, 0, cs.Password);
313311
payload = new PayloadData(new ArraySegment<byte>(hashedPassword));
314312
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
315-
break;
313+
return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
316314

317315
case "mysql_clear_password":
318316
if (!m_isSecureConnection)
319317
throw new MySqlException("Authentication method '{0}' requires a secure connection.".FormatInvariant(switchRequest.Name));
320318
payload = new PayloadData(new ArraySegment<byte>(Encoding.UTF8.GetBytes(cs.Password)));
321319
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
322-
break;
320+
return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
323321

324-
case "sha256_password":
325-
// add NUL terminator to password
326-
var passwordBytes = Encoding.UTF8.GetBytes(cs.Password);
327-
Array.Resize(ref passwordBytes, passwordBytes.Length + 1);
322+
case "caching_sha2_password":
323+
var scrambleBytes = AuthenticationUtility.CreateScrambleResponse(Utility.TrimZeroByte(switchRequest.Data), cs.Password);
324+
payload = new PayloadData(new ArraySegment<byte>(scrambleBytes));
325+
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
326+
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
327+
328+
var cachingSha2ServerResponsePayload = CachingSha2ServerResponsePayload.Create(payload);
328329

329-
if (!m_isSecureConnection && passwordBytes.Length > 1)
330+
if (cachingSha2ServerResponsePayload.Succeeded)
331+
{
332+
return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
333+
}
334+
335+
if (!m_isSecureConnection && cs.Password.Length > 1)
330336
{
331337
#if NET45
332338
throw new MySqlException("Authentication method '{0}' requires a secure connection (prior to .NET 4.6).".FormatInvariant(switchRequest.Name));
333339
#else
334-
string publicKey;
335-
if (!string.IsNullOrEmpty(cs.ServerRsaPublicKeyFile))
336-
{
337-
try
338-
{
339-
publicKey = File.ReadAllText(cs.ServerRsaPublicKeyFile);
340-
}
341-
catch (IOException ex)
342-
{
343-
throw new MySqlException("Couldn't load server's RSA public key from '{0}'".FormatInvariant(cs.ServerRsaPublicKeyFile), ex);
344-
}
345-
}
346-
else if (cs.AllowPublicKeyRetrieval)
347-
{
348-
// request the RSA public key
349-
await SendReplyAsync(new PayloadData(new ArraySegment<byte>(new byte[] { 0x01 }, 0, 1)), ioBehavior, cancellationToken).ConfigureAwait(false);
350-
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
351-
var publicKeyPayload = AuthenticationMoreDataPayload.Create(payload);
352-
publicKey = Encoding.ASCII.GetString(publicKeyPayload.Data);
353-
}
354-
else
355-
{
356-
throw new MySqlException("Authentication method '{0}' failed. Either use a secure connection, specify the server's RSA public key with ServerRSAPublicKeyFile, or set AllowPublicKeyRetrieval=True.".FormatInvariant(switchRequest.Name));
357-
}
358340

359-
// load the RSA public key
360-
RSA rsa;
361-
try
362-
{
363-
rsa = Utility.DecodeX509PublicKey(publicKey);
364-
}
365-
catch (Exception ex)
366-
{
367-
throw new MySqlException("Couldn't load server's RSA public key; try using a secure connection instead.", ex);
368-
}
341+
var rsaPublicKey = await GetRsaPublicKeyForCachingSha2PasswordAsync(switchRequest.Name, cs, ioBehavior, cancellationToken);
342+
return await SendEncryptedPasswordAsync(rsaPublicKey, RSAEncryptionPadding.Pkcs1, cs, ioBehavior, switchRequest, cancellationToken);
343+
#endif
344+
}
345+
else
346+
{
347+
return await SendClearPasswordAsync(cs, ioBehavior, cancellationToken);
348+
}
369349

370-
using (rsa)
371-
{
372-
// XOR the password bytes with the challenge
373-
AuthPluginData = Utility.TrimZeroByte(switchRequest.Data);
374-
for (int i = 0; i < passwordBytes.Length; i++)
375-
passwordBytes[i] ^= AuthPluginData[i % AuthPluginData.Length];
376-
377-
// encrypt with RSA public key
378-
var encryptedPassword = rsa.Encrypt(passwordBytes, RSAEncryptionPadding.OaepSHA1);
379-
payload = new PayloadData(new ArraySegment<byte>(encryptedPassword));
380-
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
381-
}
350+
case "sha256_password":
351+
if (!m_isSecureConnection && cs.Password.Length > 1)
352+
{
353+
#if NET45
354+
throw new MySqlException("Authentication method '{0}' requires a secure connection (prior to .NET 4.6).".FormatInvariant(switchRequest.Name));
355+
#else
356+
var publicKey = await GetRsaPublicKeyForSha256PasswordAsync(switchRequest.Name, cs, ioBehavior, cancellationToken);
357+
return await SendEncryptedPasswordAsync(publicKey, RSAEncryptionPadding.OaepSHA1, cs, ioBehavior, switchRequest, cancellationToken);
382358
#endif
383359
}
384360
else
385361
{
386-
// send plaintext password
387-
payload = new PayloadData(new ArraySegment<byte>(passwordBytes));
388-
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
362+
return await SendClearPasswordAsync(cs, ioBehavior, cancellationToken);
389363
}
390-
break;
391364

392365
case "mysql_old_password":
393366
throw new NotSupportedException("'MySQL Server is requesting the insecure pre-4.1 auth mechanism (mysql_old_password). The user password must be upgraded; see https://dev.mysql.com/doc/refman/5.7/en/account-upgrades.html.");
@@ -397,6 +370,126 @@ private async Task SwitchAuthenticationAsync(ConnectionSettings cs, PayloadData
397370
}
398371
}
399372

373+
private async Task<PayloadData> SendClearPasswordAsync(ConnectionSettings cs, IOBehavior ioBehavior, CancellationToken cancellationToken)
374+
{
375+
// add NUL terminator to password
376+
var passwordBytes = Encoding.UTF8.GetBytes(cs.Password);
377+
Array.Resize(ref passwordBytes, passwordBytes.Length + 1);
378+
379+
// send plaintext password
380+
var payload = new PayloadData(new ArraySegment<byte>(passwordBytes));
381+
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
382+
return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
383+
}
384+
385+
#if !NET45
386+
private async Task<PayloadData> SendEncryptedPasswordAsync(
387+
string rsaPublicKey,
388+
RSAEncryptionPadding rsaEncryptionPadding,
389+
ConnectionSettings cs,
390+
IOBehavior ioBehavior,
391+
AuthenticationMethodSwitchRequestPayload switchRequest,
392+
CancellationToken cancellationToken)
393+
{
394+
// load the RSA public key
395+
RSA rsa;
396+
try
397+
{
398+
rsa = Utility.DecodeX509PublicKey(rsaPublicKey);
399+
}
400+
catch (Exception ex)
401+
{
402+
throw new MySqlException("Couldn't load server's RSA public key; try using a secure connection instead.", ex);
403+
}
404+
405+
// add NUL terminator to password
406+
var passwordBytes = Encoding.UTF8.GetBytes(cs.Password);
407+
Array.Resize(ref passwordBytes, passwordBytes.Length + 1);
408+
409+
using (rsa)
410+
{
411+
// XOR the password bytes with the challenge
412+
AuthPluginData = Utility.TrimZeroByte(switchRequest.Data);
413+
for (var i = 0; i < passwordBytes.Length; i++)
414+
passwordBytes[i] ^= AuthPluginData[i % AuthPluginData.Length];
415+
416+
// encrypt with RSA public key
417+
var encryptedPassword = rsa.Encrypt(passwordBytes, rsaEncryptionPadding);
418+
var payload = new PayloadData(new ArraySegment<byte>(encryptedPassword));
419+
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
420+
return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
421+
}
422+
}
423+
#endif
424+
425+
private async Task<string> GetRsaPublicKeyForSha256PasswordAsync(
426+
string switchRequestName,
427+
ConnectionSettings cs,
428+
IOBehavior ioBehavior,
429+
CancellationToken cancellationToken)
430+
{
431+
if (!string.IsNullOrEmpty(cs.ServerRsaPublicKeyFile))
432+
{
433+
try
434+
{
435+
return File.ReadAllText(cs.ServerRsaPublicKeyFile);
436+
}
437+
catch (IOException ex)
438+
{
439+
throw new MySqlException(
440+
"Couldn't load server's RSA public key from '{0}'".FormatInvariant(cs.ServerRsaPublicKeyFile), ex);
441+
}
442+
}
443+
444+
if (cs.AllowPublicKeyRetrieval)
445+
{
446+
// request the RSA public key
447+
await SendReplyAsync(new PayloadData(new ArraySegment<byte>(new byte[] { 0x01 }, 0, 1)), ioBehavior,
448+
cancellationToken).ConfigureAwait(false);
449+
var payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
450+
var publicKeyPayload = AuthenticationMoreDataPayload.Create(payload);
451+
return Encoding.ASCII.GetString(publicKeyPayload.Data);
452+
}
453+
454+
throw new MySqlException(
455+
"Authentication method '{0}' failed. Either use a secure connection, specify the server's RSA public key with ServerRSAPublicKeyFile, or set AllowPublicKeyRetrieval=True."
456+
.FormatInvariant(switchRequestName));
457+
}
458+
459+
private async Task<string> GetRsaPublicKeyForCachingSha2PasswordAsync(
460+
string switchRequestName,
461+
ConnectionSettings cs,
462+
IOBehavior ioBehavior,
463+
CancellationToken cancellationToken)
464+
{
465+
if (!string.IsNullOrEmpty(cs.ServerRsaPublicKeyFile))
466+
{
467+
try
468+
{
469+
return File.ReadAllText(cs.ServerRsaPublicKeyFile);
470+
}
471+
catch (IOException ex)
472+
{
473+
throw new MySqlException(
474+
"Couldn't load server's RSA public key from '{0}'".FormatInvariant(cs.ServerRsaPublicKeyFile), ex);
475+
}
476+
}
477+
478+
if (cs.AllowPublicKeyRetrieval)
479+
{
480+
// request the RSA public key
481+
await SendReplyAsync(new PayloadData(new ArraySegment<byte>(new byte[] { 0x02 }, 0, 1)), ioBehavior,
482+
cancellationToken).ConfigureAwait(false);
483+
var payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
484+
var publicKeyPayload = AuthenticationMoreDataPayload.Create(payload);
485+
return Encoding.ASCII.GetString(publicKeyPayload.Data);
486+
}
487+
488+
throw new MySqlException(
489+
"Authentication method '{0}' failed. Either use a secure connection, specify the server's RSA public key with ServerRSAPublicKeyFile, or set AllowPublicKeyRetrieval=True."
490+
.FormatInvariant(switchRequestName));
491+
}
492+
400493
public async Task<bool> TryPingAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
401494
{
402495
VerifyState(State.Connected);

0 commit comments

Comments
 (0)