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

Provide Argon2id overloads to pass password hashes as strings #15

Merged
merged 2 commits into from
Dec 29, 2024
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
73 changes: 73 additions & 0 deletions src/Geralt.Tests/Argon2idTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,77 @@ public void NeedsRehash_Invalid(int hashSize, int iterations, int memorySize)

Assert.ThrowsException<ArgumentOutOfRangeException>(() => Argon2id.NeedsRehash(h, iterations, memorySize));
}

[TestMethod]
[DataRow("correct horse battery staple", Argon2id.MinIterations, Argon2id.MinMemorySize)]
public void ComputeHash_String_Valid(string password, int iterations, int memorySize)
{
Span<byte> p = Encoding.UTF8.GetBytes(password);

string h = Argon2id.ComputeHash(p, iterations, memorySize);

Assert.IsNotNull(h);

bool valid = Argon2id.VerifyHash(h, p);
Assert.IsTrue(valid);

bool rehash = Argon2id.NeedsRehash(h, iterations, memorySize);
Assert.IsFalse(rehash);
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing ComputeHash_String_Invalid, not that there's much to test.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are also no NeedsRehash tests.

[TestMethod]
[DynamicData(nameof(StringTestVectors), DynamicDataSourceType.Method)]
public void VerifyHash_String_Valid(bool expected, string hash, string password)
{
Span<byte> p = Encoding.UTF8.GetBytes(password);

bool valid = Argon2id.VerifyHash(hash, p);

Assert.AreEqual(expected, valid);
}

[TestMethod]
[DataRow("$argon2i$v=19$m=4096,t=3,p=1$eXNtbzQwOTFzajAwMDAwMA$Bb7qAql9aguCTBpLP4PVnlBd+ehJ5rX0R7smB/FggOM", "password")]
[DataRow("$argon2d$v=19$m=4096,t=3,p=1$YTBxd2k1bXBhZHIwMDAwMA$3MM5BChSl8q+MQED0fql0nwP5ykjHdBrGE0mVJHFEUE", "password")]
public void VerifyHash_String_Tampered(string hash, string password)
{
var p = Encoding.UTF8.GetBytes(password);

Assert.ThrowsException<FormatException>(() => Argon2id.VerifyHash(hash, p));
}

[TestMethod]
[DataRow("$argon2id$", "")]
[DataRow("$argon2id$v=1", "")]
[DataRow("$argon2id$v=19", "")]
[DataRow("$argon2id$v=19$", "")]
[DataRow("$argon2id$v=19$m=4882,t=", "")]
[DataRow("$argon2id$v=19$m=4882,t=2,p=1$", "")]
[DataRow("$argon2id$v=19$m=4882,t=2,p=1$bA81arsiX", "")]
[DataRow("$argon2id$v=19$m=4882,t=2,p=1$bA81arsiXysd3WbTRzmEOw$Nm8QBM+7", "")]
[DataRow("$argon2id$v=19$m=4882,t=2,p=1$bA81arsiXysd3WbTRzmEOw$Nm8QBM+7RH1DXo9rvp5cwKEOOOfD2g6JuxlXihoNcp", "")]
public void VerifyHash_String_Invalid(string hash, string password)
{
var p = Encoding.UTF8.GetBytes(password);

bool valid = Argon2id.VerifyHash(hash, p);
Assert.IsFalse(valid);
}

[TestMethod]
[DataRow("$argon2id$", "")]
[DataRow("$argon2id$v=1", "")]
[DataRow("$argon2id$v=19", "")]
[DataRow("$argon2id$v=19$", "")]
[DataRow("$argon2id$v=19$m=4882,t=", "")]
[DataRow("$argon2id$v=19$m=4882,t=2,p=1$", "")]
[DataRow("$argon2id$v=19$m=4882,t=2,p=1$bA81arsiX", "")]
[DataRow("$argon2id$v=19$m=4882,t=2,p=1$bA81arsiXysd3WbTRzmEOw$Nm8QBM+7", "")]
[DataRow("$argon2id$v=19$m=4882,t=2,p=1$bA81arsiXysd3WbTRzmEOw$Nm8QBM+7RH1DXo9rvp5cwKEOOOfD2g6JuxlXihoNcp", "")]
public void NeedsRehash_String_Invalid(string hash, string password)
{
var p = Encoding.UTF8.GetBytes(password);

Assert.ThrowsException<FormatException>(() => Argon2id.NeedsRehash(hash, Argon2id.MinIterations, Argon2id.MinMemorySize));
}
}
45 changes: 44 additions & 1 deletion src/Geralt/Crypto/Argon2id.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text;
using System.Runtime.InteropServices;
using System.Text;
using static Interop.Libsodium;

namespace Geralt;
Expand Down Expand Up @@ -35,6 +36,22 @@ public static void ComputeHash(Span<byte> hash, ReadOnlySpan<byte> password, int
if (ret != 0) { throw new InsufficientMemoryException("Insufficient memory to perform password hashing."); }
}

public static string ComputeHash(ReadOnlySpan<byte> password, int iterations, int memorySize)
{
Validation.NotLessThanMin(nameof(iterations), iterations, MinIterations);
Validation.NotLessThanMin(nameof(memorySize), memorySize, MinMemorySize);
Sodium.Initialize();
nint hash = Marshal.AllocHGlobal(MaxHashSize);
try {
int ret = crypto_pwhash_str_alg(hash, password, (ulong)password.Length, (ulong)iterations, (nuint)memorySize, crypto_pwhash_argon2id_ALG_ARGON2ID13);
if (ret != 0) { throw new InsufficientMemoryException("Insufficient memory to perform password hashing."); }
return Marshal.PtrToStringAnsi(hash)!;
}
finally {
Marshal.FreeHGlobal(hash);
}
}

public static bool VerifyHash(ReadOnlySpan<byte> hash, ReadOnlySpan<byte> password)
{
Validation.SizeBetween(nameof(hash), hash.Length, MinHashSize, MaxHashSize);
Expand All @@ -43,6 +60,14 @@ public static bool VerifyHash(ReadOnlySpan<byte> hash, ReadOnlySpan<byte> passwo
return crypto_pwhash_str_verify(hash, password, (ulong)password.Length) == 0;
}

public static bool VerifyHash(string hash, ReadOnlySpan<byte> password)
{
Validation.NotNull(nameof(hash), hash);
ThrowIfInvalidHashPrefix(hash);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please change this to Validation.NotNullOrEmpty().

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, it's probably better to do Validation.NotNull() and then Validation.SizeBetween() with the MinHashSize and MaxHashSize constants.

Sodium.Initialize();
return crypto_pwhash_str_verify(hash, password, (ulong)password.Length) == 0;
}

public static bool NeedsRehash(ReadOnlySpan<byte> hash, int iterations, int memorySize)
{
Validation.SizeBetween(nameof(hash), hash.Length, MinHashSize, MaxHashSize);
Expand All @@ -54,10 +79,28 @@ public static bool NeedsRehash(ReadOnlySpan<byte> hash, int iterations, int memo
return ret == -1 ? throw new FormatException("Invalid encoded password hash.") : ret == 1;
}

public static bool NeedsRehash(string hash, int iterations, int memorySize)
{
Validation.NotNull(nameof(hash), hash);
Validation.NotLessThanMin(nameof(iterations), iterations, MinIterations);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please change this to Validation.NotNullOrEmpty().

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, it's probably better to do Validation.NotNull() and then Validation.SizeBetween() with the MinHashSize and MaxHashSize constants.

Validation.NotLessThanMin(nameof(memorySize), memorySize, MinMemorySize);
ThrowIfInvalidHashPrefix(hash);
Sodium.Initialize();
int ret = crypto_pwhash_str_needs_rehash(hash, (ulong)iterations, (nuint)memorySize);
return ret == -1 ? throw new FormatException("Invalid encoded password hash.") : ret == 1;
}

private static void ThrowIfInvalidHashPrefix(ReadOnlySpan<byte> hash)
{
if (!ConstantTime.Equals(hash[..HashPrefix.Length], Encoding.UTF8.GetBytes(HashPrefix))) {
throw new FormatException("Invalid encoded password hash prefix.");
}
}

private static void ThrowIfInvalidHashPrefix(string hash)
{
if (!hash.StartsWith(HashPrefix)) {
throw new FormatException("Invalid encoded password hash prefix.");
}
}
}
12 changes: 12 additions & 0 deletions src/Geralt/Interop/Interop.Argon2id.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,24 @@ internal static partial class Libsodium
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
internal static partial int crypto_pwhash_str_alg(Span<byte> hash, ReadOnlySpan<byte> password, ulong passwordLength, ulong iterations, nuint memorySize, int algorithm);

[LibraryImport(DllName)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
internal static partial int crypto_pwhash_str_alg(nint hash, ReadOnlySpan<byte> password, ulong passwordLength, ulong iterations, nuint memorySize, int algorithm);

[LibraryImport(DllName)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
internal static partial int crypto_pwhash_str_verify(ReadOnlySpan<byte> hash, ReadOnlySpan<byte> password, ulong passwordLength);

[LibraryImport(DllName)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
internal static partial int crypto_pwhash_str_verify([MarshalAs(UnmanagedType.LPStr)] string hash, ReadOnlySpan<byte> password, ulong passwordLength);

[LibraryImport(DllName)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
internal static partial int crypto_pwhash_str_needs_rehash(ReadOnlySpan<byte> hash, ulong iterations, nuint memorySize);

[LibraryImport(DllName)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
internal static partial int crypto_pwhash_str_needs_rehash([MarshalAs(UnmanagedType.LPStr)] string hash, ulong iterations, nuint memorySize);
}
}
Loading