Skip to content

Commit b77322a

Browse files
authored
feat: support MD5SHA256Password auth (#5)
1 parent fbd53a1 commit b77322a

File tree

3 files changed

+87
-15
lines changed

3 files changed

+87
-15
lines changed

src/Npgsql/BackendMessages/AuthenticationMessages.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,19 @@ internal static AuthenticationRequestMessage Load(NpgsqlReadBuffer buf)
8181
}
8282
}
8383

84+
sealed class AuthenticationMD5SHA256PasswordMessage : AuthenticationRequestMessage
85+
{
86+
internal override AuthenticationRequestType AuthRequestType => AuthenticationRequestType.MD5SHA256Password;
87+
88+
internal ReadOnlyMemory<byte> Salt { get; }
89+
internal string RandomCode { get; }
90+
public AuthenticationMD5SHA256PasswordMessage(NpgsqlReadBuffer buf)
91+
{
92+
RandomCode = buf.ReadString(64);
93+
Salt = buf.ReadMemory(4);
94+
}
95+
}
96+
8497
#endregion SHA256Password
8598

8699

@@ -126,7 +139,8 @@ enum AuthenticationRequestType
126139
GSS = 7,
127140
GSSContinue = 8,
128141
SSPI = 9,
129-
SHA256Password = 10
142+
SHA256Password = 10,
143+
MD5SHA256Password = 11
130144
}
131145

132146
enum PasswordStoreType

src/Npgsql/Internal/NpgsqlConnector.Auth.cs

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ async Task Authenticate(string username, NpgsqlTimeout timeout, bool async, Canc
5151
await AuthenticateSHA256(username, (AuthenticationSHA256PasswordMessage)msg, async, cancellationToken).ConfigureAwait(false);
5252
break;
5353

54+
case AuthenticationRequestType.MD5SHA256Password:
55+
ThrowIfNotAllowed(requiredAuthModes, RequireAuthMode.ScramSHA256);
56+
await AuthenticateMD5SHA256(username, (AuthenticationMD5SHA256PasswordMessage)msg, async, cancellationToken)
57+
.ConfigureAwait(false);
58+
break;
59+
5460
case AuthenticationRequestType.GSS:
5561
case AuthenticationRequestType.SSPI:
5662
ThrowIfNotAllowed(requiredAuthModes, msg.AuthRequestType == AuthenticationRequestType.GSS ? RequireAuthMode.GSS : RequireAuthMode.SSPI);
@@ -237,22 +243,62 @@ async Task AuthenticateSHA256(string username, AuthenticationSHA256PasswordMessa
237243

238244
var result = new byte[hValue.Length * 2 + 1];
239245
BytesToHex(hValue, result, 0, hValue.Length);
240-
await WriteSHA256Response(result, async, cancellationToken).ConfigureAwait(false);
246+
await WritePassword(result, async, cancellationToken).ConfigureAwait(false);
241247
await Flush(async, cancellationToken).ConfigureAwait(false);
248+
}
249+
250+
async Task AuthenticateMD5SHA256(string username, AuthenticationMD5SHA256PasswordMessage message, bool async, CancellationToken cancellationToken = default)
251+
{
252+
var password = await GetPassword(username, async, cancellationToken).ConfigureAwait(false);
253+
if (string.IsNullOrEmpty(password))
254+
throw new NpgsqlException("No password has been provided but the backend requires one (in SHA256)");
255+
256+
var passwordBytes = NpgsqlWriteBuffer.UTF8Encoding.GetBytes(password);
257+
258+
// https://github.com/HuaweiCloudDeveloper/gaussdb-r2dbc/blob/54783aa7ba09731300b31d9cf366185d0bf50447/src/main/java/io/r2dbc/gaussdb/util/MD5Digest.java#L227
259+
var randomCodeBytes = Convert.FromHexString(message.RandomCode);
260+
261+
var passwordKeyBytes = Rfc2898DeriveBytes.Pbkdf2(passwordBytes, randomCodeBytes,
262+
2048, HashAlgorithmName.SHA1, 32
263+
);
264+
265+
var serverKey = HMACSHA256.HashData(passwordKeyBytes, "Sever Key"u8);
266+
var clientKey = HMACSHA256.HashData(passwordKeyBytes, "Client Key"u8);
267+
var storedKey = SHA256.HashData(clientKey);
268+
269+
var stringBuilder = new StringBuilder();
270+
stringBuilder.Append(message.RandomCode);
271+
stringBuilder.Append(Convert.ToHexString(serverKey).ToLowerInvariant());
272+
stringBuilder.Append(Convert.ToHexString(storedKey).ToLowerInvariant());
273+
var encryptedString = stringBuilder.ToString();
242274

243-
static void BytesToHex(byte[] bytes, byte[] hex, int offset, int length)
275+
byte[] passDigest;
276+
using (var md5 = MD5.Create())
244277
{
245-
var lookup = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
246-
var pos = offset;
247-
for (var i = 0; i < length; ++i)
248-
{
249-
var c = bytes[i] & 255;
250-
var j = c >> 4;
251-
hex[pos++] = (byte)lookup[j];
252-
j = c & 15;
253-
hex[pos++] = (byte)lookup[j];
254-
}
278+
// Convert the string to bytes using UTF-8 encoding
279+
var stringBytes = NpgsqlWriteBuffer.UTF8Encoding.GetBytes(encryptedString);
280+
281+
// Update the hash state with the string bytes
282+
// The 'null, 0' arguments are for the output buffer, which isn't needed here
283+
md5.TransformBlock(stringBytes, 0, stringBytes.Length, null, 0);
284+
285+
// Update the hash state with the salt bytes and finalize the hash calculation
286+
// This is the final block of data being added.
287+
var saltBytes = message.Salt.ToArray();
288+
md5.TransformFinalBlock(saltBytes, 0, saltBytes.Length);
289+
290+
// Retrieve the computed hash digest
291+
ArgumentNullException.ThrowIfNull(md5.Hash);
292+
passDigest = md5.Hash;
255293
}
294+
295+
var result = new byte[MD5.HashSizeInBytes * 2 + 3];
296+
result[0] = (byte)'m';
297+
result[1] = (byte)'d';
298+
result[2] = (byte)'5';
299+
BytesToHex(passDigest, result, 3, MD5.HashSizeInBytes);
300+
await WritePassword(result, async, cancellationToken).ConfigureAwait(false);
301+
await Flush(async, cancellationToken).ConfigureAwait(false);
256302
}
257303

258304
internal async Task AuthenticateGSS(bool async)
@@ -325,4 +371,18 @@ internal async Task AuthenticateGSS(bool async)
325371

326372
return password;
327373
}
374+
375+
static void BytesToHex(byte[] bytes, byte[] hex, int offset, int length)
376+
{
377+
var lookup = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
378+
var pos = offset;
379+
for (var i = 0; i < length; ++i)
380+
{
381+
var c = bytes[i] & 255;
382+
var j = c >> 4;
383+
hex[pos++] = (byte)lookup[j];
384+
j = c & 15;
385+
hex[pos++] = (byte)lookup[j];
386+
}
387+
}
328388
}

src/Npgsql/Internal/NpgsqlConnector.FrontendMessages.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -451,8 +451,6 @@ internal async Task WritePassword(byte[] payload, int offset, int count, bool as
451451
await WriteBuffer.DirectWrite(new ReadOnlyMemory<byte>(payload, offset, count), async, cancellationToken).ConfigureAwait(false);
452452
}
453453

454-
internal Task WriteSHA256Response(byte[] payload, bool async, CancellationToken cancellationToken = default) => WritePassword(payload, async, cancellationToken);
455-
456454
#endregion Authentication
457455

458456
internal Task WritePregenerated(byte[] data, bool async = false, CancellationToken cancellationToken = default)

0 commit comments

Comments
 (0)