Skip to content

Commit

Permalink
Provide Argon2id overloads to pass password hashes as strings (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
ektrah authored Dec 29, 2024
1 parent 41972af commit f8e583a
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 1 deletion.
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);
}

[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);
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);
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);
}
}

0 comments on commit f8e583a

Please sign in to comment.