Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/SuperSocket.MySQL/MySQLConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ private byte[] GenerateNativePasswordResponse(byte[] salt)
}
}

private byte[] GenerateCachingSha2Response(byte[] salt)
internal byte[] GenerateCachingSha2Response(byte[] salt)
{
if (string.IsNullOrEmpty(_password))
return Array.Empty<byte>();
Expand Down Expand Up @@ -383,4 +383,4 @@ protected override void OnClosed(object sender, EventArgs e)
base.OnClosed(sender, e);
}
}
}
}
35 changes: 35 additions & 0 deletions tests/SuperSocket.MySQL.Test/HandshakeTest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Xunit;
Expand Down Expand Up @@ -202,6 +203,40 @@ public void MySQLConnection_GenerateAuthResponse_ShouldHandleDifferentPasswords(
Assert.NotNull(connection);
}

[Fact]
public void MySQLConnection_GenerateCachingSha2Response_ShouldMatchExpected()
{
// Arrange
const string password = "test_password";
var connection = new MySQLConnection("localhost", 3306, "user", password);
// Include a trailing null byte to validate trimming of the salt.
var salt = new byte[] { 0x33, 0x21, 0x55, 0x42, 0x19, 0x76, 0xA1, 0x0B, 0x10, 0x5C, 0x2D, 0x48, 0x5A, 0x00 };

// Act
var response = connection.GenerateCachingSha2Response(salt);

// Assert
var trimmedSaltLength = salt[^1] == 0 ? salt.Length - 1 : salt.Length;
var trimmedSalt = new byte[trimmedSaltLength];
Array.Copy(salt, trimmedSalt, trimmedSaltLength);

using var sha256 = SHA256.Create();
var passwordBytes = Encoding.UTF8.GetBytes(password);
var sha256Password = sha256.ComputeHash(passwordBytes);
var sha256Sha256Password = sha256.ComputeHash(sha256Password);
var hashAndSalt = new byte[sha256Sha256Password.Length + trimmedSalt.Length];
Array.Copy(sha256Sha256Password, 0, hashAndSalt, 0, sha256Sha256Password.Length);
Array.Copy(trimmedSalt, 0, hashAndSalt, sha256Sha256Password.Length, trimmedSalt.Length);
var sha256Combined = sha256.ComputeHash(hashAndSalt);
var expected = new byte[sha256Password.Length];
for (int i = 0; i < expected.Length; i++)
{
expected[i] = (byte)(sha256Password[i] ^ sha256Combined[i]);
}

Assert.Equal(expected, response);
}

[Fact]
public void EOFPacket_ShouldNotIndicateAuthenticationSuccess()
{
Expand Down
46 changes: 46 additions & 0 deletions tests/SuperSocket.MySQL.Test/MySQLIntegrationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,52 @@ public async Task MySQLConnection_InvalidCredentials_ShouldFailHandshake()
"Connection should not be authenticated after failed handshake");
}

[Fact]
[Trait("Category", "Integration")]
public async Task MySQLConnection_CachingSha2PasswordUser_ShouldAuthenticate()
{
var userName = $"sha2_user_{Guid.NewGuid():N}";
var password = $"sha2_pass_{Guid.NewGuid():N}";
var host = TestConst.Host;
var adminConnection = new MySQLConnection(host, TestConst.DefaultPort, TestConst.Username, TestConst.Password);
MySQLConnection userConnection = null;

try
{
await adminConnection.ConnectAsync();

var pluginResult = await adminConnection.ExecuteQueryAsync(
"SELECT PLUGIN_NAME FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME='caching_sha2_password' AND PLUGIN_STATUS='ACTIVE'");

if (!pluginResult.IsSuccess || pluginResult.RowCount == 0)
return;

await adminConnection.ExecuteQueryAsync(
$"CREATE USER '{userName}'@'{host}' IDENTIFIED WITH caching_sha2_password BY '{password}'");
await adminConnection.ExecuteQueryAsync($"GRANT USAGE ON *.* TO '{userName}'@'{host}'");

userConnection = new MySQLConnection(host, TestConst.DefaultPort, userName, password);
await userConnection.ConnectAsync();

Assert.True(userConnection.IsAuthenticated,
"Connection should be authenticated using caching_sha2_password.");
}
finally
{
if (userConnection != null)
{
await userConnection.DisconnectAsync();
}

if (adminConnection.IsAuthenticated)
{
await adminConnection.ExecuteQueryAsync($"DROP USER IF EXISTS '{userName}'@'{host}'");
}

await adminConnection.DisconnectAsync();
}
}

[Fact]
[Trait("Category", "Integration")]
public async Task MySQLConnection_ConcurrentConnections_ShouldWork()
Expand Down