@@ -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