Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using TLS without configuration #1500

Merged
merged 21 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3ea4d86
Changing Permit redirection
rusher Jul 20, 2024
4e58e42
Allow 'localhost' as a server address.
bgrainger Jul 21, 2024
e7b1fc7
permit skipping redirection test
rusher Jul 23, 2024
89beadf
Using TLS without configuration
rusher Jul 26, 2024
098bd5b
Merge master into ssl.
bgrainger Jul 28, 2024
d429b35
Delete setup step that should be done outside of tests.
bgrainger Jul 28, 2024
45dddae
Delete unnecessary attribute constructor.
bgrainger Jul 28, 2024
aff3418
Make Ed25519AuthenticationPlugin.Install threadsafe.
bgrainger Jul 28, 2024
ec7f8e0
Use existing test accounts instead of creating new ones.
bgrainger Jul 28, 2024
c354752
Fix MySql.Data build.
bgrainger Jul 28, 2024
003934c
Delete SessionConnectionString property.
bgrainger Jul 28, 2024
71e680a
Revert word-wrapping.
bgrainger Jul 28, 2024
43ee64d
Allow SSL tests to pass via fingerprint validation.
bgrainger Jul 28, 2024
4a99d6a
Add IAuthenticationMethod2 interface to avoid breaking change.
bgrainger Jul 28, 2024
03d179e
Defer copy of challenge until it's needed.
bgrainger Jul 28, 2024
2aea924
Change parameter name for clarity and update documentation.
bgrainger Jul 28, 2024
b4d8baf
Eliminate allocations when computing fingerprint hash.
bgrainger Jul 28, 2024
b58af51
Optimise thumbprint creation under .NET 7.
bgrainger Jul 28, 2024
34524ef
Restore AuthenticationException as inner exception.
bgrainger Jul 28, 2024
abee0c7
Add logging for provisional TLS connection and failure reasons.
bgrainger Jul 28, 2024
7c2964f
Minor code cleanup.
bgrainger Jul 28, 2024
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
2 changes: 1 addition & 1 deletion .ci/config/config.compression+ssl.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
"PasswordlessUser": "no_password",
"SecondaryDatabase": "testdb2",
"UnsupportedFeatures": "CachingSha2Password,Redirection,RsaEncryption,Tls12,Tls13,UuidToBin",
"UnsupportedFeatures": "CachingSha2Password,Redirection,RsaEncryption,Tls12,Tls13,TlsFingerprintValidation,UuidToBin",
"MySqlBulkLoaderLocalCsvFile": "../../../TestData/LoadData_UTF8_BOM_Unix.CSV",
"MySqlBulkLoaderLocalTsvFile": "../../../TestData/LoadData_UTF8_BOM_Unix.TSV",
"CertificatesPath": "../../../../.ci/server/certs"
Expand Down
2 changes: 1 addition & 1 deletion .ci/config/config.compression.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
"PasswordlessUser": "no_password",
"SecondaryDatabase": "testdb2",
"UnsupportedFeatures": "Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,UnixDomainSocket,ZeroDateTime",
"UnsupportedFeatures": "Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket,ZeroDateTime",
"MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV",
"MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV"
}
Expand Down
2 changes: 1 addition & 1 deletion .ci/config/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
"PasswordlessUser": "no_password",
"SecondaryDatabase": "testdb2",
"UnsupportedFeatures": "Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,UnixDomainSocket,ZeroDateTime",
"UnsupportedFeatures": "Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket,ZeroDateTime",
"MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV",
"MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV"
}
Expand Down
2 changes: 1 addition & 1 deletion .ci/config/config.ssl.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
"PasswordlessUser": "no_password",
"SecondaryDatabase": "testdb2",
"UnsupportedFeatures": "CachingSha2Password,Redirection,RsaEncryption,Tls12,Tls13,UuidToBin",
"UnsupportedFeatures": "CachingSha2Password,Redirection,RsaEncryption,Tls12,Tls13,TlsFingerprintValidation,UuidToBin",
"MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV",
"MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV",
"CertificatesPath": "../../../../.ci/server/certs"
Expand Down
10 changes: 5 additions & 5 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -187,23 +187,23 @@ jobs:
'MySQL 8.0':
image: 'mysql:8.0'
connectionStringExtra: 'AllowPublicKeyRetrieval=True'
unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,ZeroDateTime'
unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,ZeroDateTime'
'MySQL 8.4':
image: 'mysql:8.4'
connectionStringExtra: 'AllowPublicKeyRetrieval=True'
unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,ZeroDateTime'
unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,ZeroDateTime'
'MySQL 9.0':
image: 'mysql:9.0'
connectionStringExtra: 'AllowPublicKeyRetrieval=True'
unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,ZeroDateTime'
unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,ZeroDateTime'
'MariaDB 10.6':
image: 'mariadb:10.6'
connectionStringExtra: ''
unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin,Redirection'
unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Redirection,Sha256Password,Tls11,TlsFingerprintValidation,UuidToBin'
'MariaDB 10.11':
image: 'mariadb:10.11'
connectionStringExtra: ''
unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin,Redirection'
unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Redirection,Sha256Password,Tls11,TlsFingerprintValidation,UuidToBin'
'MariaDB 11.4':
image: 'mariadb:11.4'
connectionStringExtra: ''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,29 @@ public byte[] CreateResponse(string password, ReadOnlySpan<byte> authenticationD
return result;
}

/// <summary>
/// Creates the ed25519 password hash.
/// </summary>
public byte[] CreatePasswordHash(string password, ReadOnlySpan<byte> authenticationData)
{
byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
using var sha512 = SHA512.Create();
byte[] az = sha512.ComputeHash(passwordBytes);
ScalarOperations.sc_clamp(az, 0);

byte[] sm = new byte[64 + authenticationData.Length];
authenticationData.CopyTo(sm.AsSpan().Slice(64));
Buffer.BlockCopy(az, 32, sm, 32, 32);
sha512.ComputeHash(sm, 32, authenticationData.Length + 32);

GroupOperations.ge_scalarmult_base(out var A, az, 0);
GroupOperations.ge_p3_tobytes(sm, 32, ref A);

byte[] res = new byte[32];
Array.Copy(sm, 32, res, 0, 32);
return res;
}

private Ed25519AuthenticationPlugin()
{
}
Expand Down
10 changes: 10 additions & 0 deletions src/MySqlConnector/Authentication/IAuthenticationPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,14 @@ public interface IAuthenticationPlugin
/// Method Switch Request Packet</a>.</param>
/// <returns>The authentication response.</returns>
byte[] CreateResponse(string password, ReadOnlySpan<byte> authenticationData);

/// <summary>
/// create password hash for fingerprint verification
/// </summary>
/// <param name="password">The client's password.</param>
/// <param name="authenticationData">The authentication data supplied by the server; this is the <code>auth method data</code>
/// from the <a href="https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest">Authentication
/// Method Switch Request Packet</a>.</param>
/// <returns>password hash</returns>
byte[] CreatePasswordHash(string password, ReadOnlySpan<byte> authenticationData);
}
10 changes: 7 additions & 3 deletions src/MySqlConnector/Core/ConnectionPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,11 @@ public async ValueTask<ServerSession> GetSessionAsync(MySqlConnection connection
if (ConnectionSettings.ConnectionReset || session.DatabaseOverride is not null)
{
if (timeoutMilliseconds != 0)
session.SetTimeout(Math.Max(1, timeoutMilliseconds - Utility.GetElapsedMilliseconds(startingTimestamp)));
reuseSession = await session.TryResetConnectionAsync(ConnectionSettings, connection, ioBehavior, cancellationToken).ConfigureAwait(false);
session.SetTimeout(Math.Max(1,
timeoutMilliseconds - Utility.GetElapsedMilliseconds(startingTimestamp)));
reuseSession = await session
.TryResetConnectionAsync(ConnectionSettings, connection, ioBehavior, cancellationToken)
.ConfigureAwait(false);
session.SetTimeout(Constants.InfiniteTimeout);
}
else
Expand Down Expand Up @@ -107,7 +110,8 @@ public async ValueTask<ServerSession> GetSessionAsync(MySqlConnection connection
Log.ReturningPooledSession(m_logger, Id, session.Id, leasedSessionsCountPooled);

session.LastLeasedTimestamp = Stopwatch.GetTimestamp();
MetricsReporter.RecordWaitTime(this, Utility.GetElapsedSeconds(startingTimestamp, session.LastLeasedTimestamp));
MetricsReporter.RecordWaitTime(this,
Utility.GetElapsedSeconds(startingTimestamp, session.LastLeasedTimestamp));
return session;
}
}
Expand Down
118 changes: 115 additions & 3 deletions src/MySqlConnector/Core/ServerSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
using MySqlConnector.Protocol.Payloads;
using MySqlConnector.Protocol.Serialization;
using MySqlConnector.Utilities;
#if NET5_0_OR_GREATER
using System.Runtime.CompilerServices;
#endif

namespace MySqlConnector.Core;

Expand Down Expand Up @@ -46,6 +49,7 @@ public ServerSession(ILogger logger, IConnectionPoolMetadata pool)
public bool SupportsPerQueryVariables => ServerVersion.IsMariaDb && ServerVersion.Version >= ServerVersions.MariaDbSupportsPerQueryVariables;
public int ActiveCommandId { get; private set; }
public int CancellationTimeout { get; private set; }
public string? ConnectionString { get; private set; }
public int ConnectionId { get; set; }
public byte[]? AuthPluginData { get; set; }
public long CreatedTimestamp { get; }
Expand Down Expand Up @@ -398,16 +402,16 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella

// set activity tags
{
var connectionString = cs.ConnectionStringBuilder.GetConnectionString(cs.ConnectionStringBuilder.PersistSecurityInfo);
ConnectionString = cs.ConnectionStringBuilder.GetConnectionString(cs.ConnectionStringBuilder.PersistSecurityInfo);
m_activityTags.Add(ActivitySourceHelper.DatabaseSystemTagName, ActivitySourceHelper.DatabaseSystemValue);
m_activityTags.Add(ActivitySourceHelper.DatabaseConnectionStringTagName, connectionString);
m_activityTags.Add(ActivitySourceHelper.DatabaseConnectionStringTagName, ConnectionString);
m_activityTags.Add(ActivitySourceHelper.DatabaseUserTagName, cs.UserID);
if (cs.Database.Length != 0)
m_activityTags.Add(ActivitySourceHelper.DatabaseNameTagName, cs.Database);
if (activity is { IsAllDataRequested: true })
{
activity.SetTag(ActivitySourceHelper.DatabaseSystemTagName, ActivitySourceHelper.DatabaseSystemValue)
.SetTag(ActivitySourceHelper.DatabaseConnectionStringTagName, connectionString)
.SetTag(ActivitySourceHelper.DatabaseConnectionStringTagName, ConnectionString)
.SetTag(ActivitySourceHelper.DatabaseUserTagName, cs.UserID);
if (cs.Database.Length != 0)
activity.SetTag(ActivitySourceHelper.DatabaseNameTagName, cs.Database);
Expand Down Expand Up @@ -528,6 +532,44 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
}

var ok = OkPayload.Create(payload.Span, this);
if (m_rcbPolicyErrors != SslPolicyErrors.None)
{
// SSL would normally have thrown error, so connector need to ensure server certificates
// pass only if :
// * connection method is MitM-proof (e.g. unix socket)
// * auth plugin is MitM-proof and check SHA2(user's hashed password, scramble, certificate fingerprint)
if (cs.ConnectionProtocol != MySqlConnectionProtocol.UnixSocket)
{
if (string.IsNullOrEmpty(password) ||
!ValidateFingerPrint(ok.StatusInfo, initialHandshake.AuthPluginData, password!))
{
// fingerprint validation fail.
// now throwing SSL exception depending on m_rcbPolicyErrors
ShutdownSocket();
HostName = "";
lock (m_lock) m_state = State.Failed;
MySqlException ex;
switch (m_rcbPolicyErrors)
{
case SslPolicyErrors.RemoteCertificateNotAvailable:
// impossible
ex = new MySqlException(MySqlErrorCode.UnableToConnectToHost, "SSL not validated, no remote certificate available");
break;

case SslPolicyErrors.RemoteCertificateNameMismatch:
ex = new MySqlException(MySqlErrorCode.UnableToConnectToHost, "SSL not validated, certificate name mismatch");
break;

default:
ex = new MySqlException(MySqlErrorCode.UnableToConnectToHost, "SSL not validated, certificate chain validation fail");
break;
}
Log.CouldNotInitializeTlsConnection(m_logger, ex, Id);
throw ex;
}
}
}

var redirectionUrl = ok.RedirectionUrl;

if (m_useCompression)
Expand Down Expand Up @@ -567,6 +609,57 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
}
}

/// <summary>
/// Validate SSL validation has
/// </summary>
/// <param name="validationHash">received validation hash</param>
/// <param name="challenge">initial seed</param>
/// <param name="password">password</param>
/// <returns>true if validated</returns>
private bool ValidateFingerPrint(byte[]? validationHash, ReadOnlySpan<byte> challenge, string password)
{
if (validationHash is null || validationHash.Length == 0) return false;

// ensure using SHA256 encryption
if (validationHash[0] != 0x01)
throw new FormatException($"Unexpected validation hash format. expected 0x01 but got 0x{validationHash[0]:X2}");

byte[] passwordHashResult;
switch (m_pluginName)
{
case "mysql_native_password":
passwordHashResult = AuthenticationUtility.HashPassword(challenge, password, false);
break;

case "client_ed25519":
AuthenticationPlugins.TryGetPlugin("client_ed25519", out var ed25519Plugin);
passwordHashResult = ed25519Plugin!.CreatePasswordHash(password, challenge);
break;

default:
return false;
}

Span<byte> combined = stackalloc byte[32 + (challenge.Length - 1) + passwordHashResult.Length];
passwordHashResult.CopyTo(combined);
challenge.CopyTo(combined[passwordHashResult.Length..]);
m_sha2Thumbprint!.CopyTo(combined[(passwordHashResult.Length + challenge.Length - 1)..]);

byte[] hashBytes;
#if NET5_0_OR_GREATER
hashBytes = SHA256.HashData(combined);
#else
using (var sha256 = SHA256.Create())
{
hashBytes = sha256.ComputeHash(combined.ToArray());
}
#endif

var clientGeneratedHash = hashBytes.Aggregate(string.Empty, (str, hashByte) => str + hashByte.ToString("X2", CultureInfo.InvariantCulture));
var serverGeneratedHash = Encoding.ASCII.GetString(validationHash, 1, validationHash.Length - 1);
return string.Equals(clientGeneratedHash, serverGeneratedHash, StringComparison.Ordinal);
}

public static async ValueTask<ServerSession> ConnectAndRedirectAsync(ILogger connectionLogger, ILogger poolLogger, IConnectionPoolMetadata pool, ConnectionSettings cs, ILoadBalancer? loadBalancer, MySqlConnection connection, Action<ILogger, int, string, Exception?>? logMessage, long startingTimestamp, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken)
{
var session = new ServerSession(connectionLogger, pool);
Expand Down Expand Up @@ -729,6 +822,7 @@ private async Task<PayloadData> SwitchAuthenticationAsync(ConnectionSettings cs,
// if the server didn't support the hashed password; rehash with the new challenge
var switchRequest = AuthenticationMethodSwitchRequestPayload.Create(payload.Span);
Log.SwitchingToAuthenticationMethod(m_logger, Id, switchRequest.Name);
m_pluginName = switchRequest.Name;
switch (switchRequest.Name)
{
case "mysql_native_password":
Expand Down Expand Up @@ -1485,6 +1579,21 @@ caCertificateChain is not null &&
if (cs.SslMode == MySqlSslMode.VerifyCA)
rcbPolicyErrors &= ~SslPolicyErrors.RemoteCertificateNameMismatch;

if (rcbCertificate is X509Certificate2 cert2)
{
// saving sha256 thumbprint and SSL errors until thumbprint validation
#if !NET5_0_OR_GREATER
using (var sha256 = SHA256.Create())
{
m_sha2Thumbprint = sha256.ComputeHash(cert2.RawData);
}
#else
m_sha2Thumbprint = SHA256.HashData(cert2.RawData);
#endif
m_rcbPolicyErrors = rcbPolicyErrors;
return true;
}

return rcbPolicyErrors == SslPolicyErrors.None;
}

Expand Down Expand Up @@ -2006,4 +2115,7 @@ protected override void OnStatementBegin(int index)
private PayloadData m_setNamesPayload;
private byte[]? m_pipelinedResetConnectionBytes;
private Dictionary<string, PreparedStatements>? m_preparedStatements;
private string m_pluginName = "mysql_native_password";
private byte[]? m_sha2Thumbprint;
private SslPolicyErrors m_rcbPolicyErrors;
}
2 changes: 2 additions & 0 deletions src/MySqlConnector/MySqlConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,8 @@ public override string ConnectionString
}
}

public string? SessionConnectionString => m_session?.ConnectionString;

public override string Database => m_session?.DatabaseOverride ?? GetConnectionSettings().Database;

public override ConnectionState State => m_connectionState;
Expand Down
6 changes: 3 additions & 3 deletions src/MySqlConnector/Protocol/Payloads/OkPayload.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ internal sealed class OkPayload
public ulong LastInsertId { get; }
public ServerStatus ServerStatus { get; }
public int WarningCount { get; }
public string? StatusInfo { get; }
public byte[]? StatusInfo { get; }
public string? NewSchema { get; }
public CharacterSet? NewCharacterSet { get; }
public int? NewConnectionId { get; }
Expand Down Expand Up @@ -152,7 +152,7 @@ public static void Verify(ReadOnlySpan<byte> span, IServerCapabilities serverCap

if (createPayload)
{
var statusInfo = statusBytes.Length == 0 ? null : Encoding.UTF8.GetString(statusBytes);
var statusInfo = statusBytes.Length == 0 ? null : statusBytes.ToArray();

// detect the connection character set as utf8mb4 (or utf8) if all three system variables are set to the same value
var characterSet = clientCharacterSet == CharacterSet.Utf8Mb4Binary && connectionCharacterSet == CharacterSet.Utf8Mb4Binary && resultsCharacterSet == CharacterSet.Utf8Mb4Binary ? CharacterSet.Utf8Mb4Binary :
Expand All @@ -175,7 +175,7 @@ public static void Verify(ReadOnlySpan<byte> span, IServerCapabilities serverCap
}
}

private OkPayload(ulong affectedRowCount, ulong lastInsertId, ServerStatus serverStatus, int warningCount, string? statusInfo, string? newSchema, CharacterSet newCharacterSet, int? connectionId, string? redirectionUrl)
private OkPayload(ulong affectedRowCount, ulong lastInsertId, ServerStatus serverStatus, int warningCount, byte[]? statusInfo, string? newSchema, CharacterSet newCharacterSet, int? connectionId, string? redirectionUrl)
{
AffectedRowCount = affectedRowCount;
LastInsertId = lastInsertId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,20 @@ public static byte[] GetNullTerminatedPasswordBytes(string password)
}

public static byte[] CreateAuthenticationResponse(ReadOnlySpan<byte> challenge, string password) =>
string.IsNullOrEmpty(password) ? [] : HashPassword(challenge, password);
string.IsNullOrEmpty(password) ? [] : HashPassword(challenge, password, true);

/// <summary>
/// Hashes a password with the "Secure Password Authentication" method.
/// </summary>
/// <param name="challenge">The 20-byte random challenge (from the "auth-plugin-data" in the initial handshake).</param>
/// <param name="password">The password to hash.</param>
/// <param name="withXor">must xor results.</param>
/// <returns>A 20-byte password hash.</returns>
/// <remarks>See <a href="https://dev.mysql.com/doc/internals/en/secure-password-authentication.html">Secure Password Authentication</a>.</remarks>
#if NET5_0_OR_GREATER
[SkipLocalsInit]
#endif
public static byte[] HashPassword(ReadOnlySpan<byte> challenge, string password)
public static byte[] HashPassword(ReadOnlySpan<byte> challenge, string password, bool withXor)
{
#if !NET5_0_OR_GREATER
using var sha1 = SHA1.Create();
Expand All @@ -56,6 +57,7 @@ public static byte[] HashPassword(ReadOnlySpan<byte> challenge, string password)
sha1.TryComputeHash(passwordBytes, hashedPassword, out _);
sha1.TryComputeHash(hashedPassword, combined[20..], out _);
#endif
if (!withXor) return combined[20..].ToArray();

Span<byte> xorBytes = stackalloc byte[20];
#if NET5_0_OR_GREATER
Expand Down
Loading
Loading