diff --git a/src/Geralt.Tests/Argon2idTests.cs b/src/Geralt.Tests/Argon2idTests.cs index de378ef..18b0d19 100644 --- a/src/Geralt.Tests/Argon2idTests.cs +++ b/src/Geralt.Tests/Argon2idTests.cs @@ -53,13 +53,12 @@ public static IEnumerable StringTestVectors() public void Constants_Valid() { Assert.AreEqual(32, Argon2id.KeySize); - Assert.AreEqual(16, Argon2id.MinKeySize); Assert.AreEqual(16, Argon2id.SaltSize); + Assert.AreEqual(16, Argon2id.MinKeySize); Assert.AreEqual(1, Argon2id.MinIterations); Assert.AreEqual(8192, Argon2id.MinMemorySize); - Assert.AreEqual(92, Argon2id.MinHashSize); + Assert.AreEqual(93, Argon2id.MinHashSize); Assert.AreEqual(128, Argon2id.MaxHashSize); - Assert.AreEqual("$argon2id$", Argon2id.HashPrefix); } [TestMethod] @@ -91,7 +90,7 @@ public void DeriveKey_Invalid(int outputKeyingMaterialSize, int passwordSize, in } [TestMethod] - [DataRow("correct horse battery staple", 3, 16777216)] + [DataRow("correct horse battery staple", Argon2id.MinIterations, Argon2id.MinMemorySize)] public void ComputeHash_Valid(string password, int iterations, int memorySize) { Span h = stackalloc byte[Argon2id.MaxHashSize]; @@ -133,6 +132,17 @@ public void VerifyHash_Valid(bool expected, string hash, string password) 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_Tampered(string hash, string password) + { + var h = Encoding.UTF8.GetBytes(hash); + var p = Encoding.UTF8.GetBytes(password); + + Assert.ThrowsException(() => Argon2id.VerifyHash(h, p)); + } + [TestMethod] [DataRow(Argon2id.MaxHashSize + 1, Argon2id.KeySize)] [DataRow(Argon2id.MinHashSize - 1, Argon2id.KeySize)] @@ -161,6 +171,8 @@ public void NeedsRehash_Valid(bool expected, string hash, int iterations, int me [DataRow("argon2id$v=19$m=16384,t=3,p=1$9jzdCOZe8dvfNWga1TS9wQ$ZdlB31msrCUY3R83w6GRGXdmq2zgUcLQGwnedCzU4Us", 3, 16777216)] [DataRow("$argon2id$v19$m=16384,t=3,p=1$9jzdCOZe8dvfNWga1TS9wQ$ZdlB31msrCUY3R83w6GRGXdmq2zgUcLQGwnedCzU4Us", 3, 16777216)] [DataRow("$argon2id$v=19$m=16384t=3,p=1$9jzdCOZe8dvfNWga1TS9wQ$ZdlB31msrCUY3R83w6GRGXdmq2zgUcLQGwnedCzU4Us", 3, 16777216)] + [DataRow("$argon2i$v=19$m=16384,t=3,p=1$9jzdCOZe8dvfNWga1TS9wQ$ZdlB31msrCUY3R83w6GRGXdmq2zgUcLQGwnedCzU4Us", 3, 16777216)] + [DataRow("$argon2d$v=19$m=16384,t=3,p=1$9jzdCOZe8dvfNWga1TS9wQ$ZdlB31msrCUY3R83w6GRGXdmq2zgUcLQGwnedCzU4Us", 3, 16777216)] public void NeedsRehash_Tampered(string hash, int iterations, int memorySize) { var h = Encoding.UTF8.GetBytes(hash); diff --git a/src/Geralt/Crypto/Argon2id.cs b/src/Geralt/Crypto/Argon2id.cs index 52a4efa..57847a3 100644 --- a/src/Geralt/Crypto/Argon2id.cs +++ b/src/Geralt/Crypto/Argon2id.cs @@ -1,23 +1,18 @@ -using static Interop.Libsodium; +using System.Text; +using static Interop.Libsodium; namespace Geralt; public static class Argon2id { public const int KeySize = 32; - public const int MinKeySize = crypto_pwhash_BYTES_MIN; public const int SaltSize = crypto_pwhash_SALTBYTES; + public const int MinKeySize = crypto_pwhash_BYTES_MIN; public const int MinIterations = crypto_pwhash_argon2id_OPSLIMIT_MIN; public const int MinMemorySize = crypto_pwhash_MEMLIMIT_MIN; - public const int MinHashSize = 92; + public const int MinHashSize = 93; public const int MaxHashSize = crypto_pwhash_STRBYTES; - public const string HashPrefix = crypto_pwhash_argon2id_STRPREFIX; - - private enum Algorithm - { - Argon2id = crypto_pwhash_argon2id_ALG_ARGON2ID13, - Argon2i = crypto_pwhash_argon2i_ALG_ARGON2I13 - } + private const string HashPrefix = crypto_pwhash_argon2id_STRPREFIX; public static unsafe void DeriveKey(Span outputKeyingMaterial, ReadOnlySpan password, ReadOnlySpan salt, int iterations, int memorySize) { @@ -28,7 +23,7 @@ public static unsafe void DeriveKey(Span outputKeyingMaterial, ReadOnlySpa Sodium.Initialize(); fixed (byte* okm = outputKeyingMaterial, p = password, s = salt) { - int ret = crypto_pwhash(okm, (ulong)outputKeyingMaterial.Length, p, (ulong)password.Length, s, (ulong)iterations, (nuint)memorySize, (int)Algorithm.Argon2id); + int ret = crypto_pwhash(okm, (ulong)outputKeyingMaterial.Length, p, (ulong)password.Length, s, (ulong)iterations, (nuint)memorySize, crypto_pwhash_argon2id_ALG_ARGON2ID13); if (ret != 0) { throw new InsufficientMemoryException("Insufficient memory to perform key derivation."); } } } @@ -41,7 +36,7 @@ public static unsafe void ComputeHash(Span hash, ReadOnlySpan passwo Sodium.Initialize(); fixed (byte* h = hash, p = password) { - int ret = crypto_pwhash_str_alg(h, p, (ulong)password.Length, (ulong)iterations, (nuint)memorySize, (int)Algorithm.Argon2id); + int ret = crypto_pwhash_str_alg(h, p, (ulong)password.Length, (ulong)iterations, (nuint)memorySize, crypto_pwhash_argon2id_ALG_ARGON2ID13); if (ret != 0) { throw new InsufficientMemoryException("Insufficient memory to perform password hashing."); } } } @@ -49,6 +44,7 @@ public static unsafe void ComputeHash(Span hash, ReadOnlySpan passwo public static unsafe bool VerifyHash(ReadOnlySpan hash, ReadOnlySpan password) { Validation.SizeBetween(nameof(hash), hash.Length, MinHashSize, MaxHashSize); + ThrowIfInvalidHashPrefix(hash); Sodium.Initialize(); fixed (byte* h = hash, p = password) return crypto_pwhash_str_verify(h, p, (ulong)password.Length) == 0; @@ -59,11 +55,18 @@ public static unsafe bool NeedsRehash(ReadOnlySpan hash, int iterations, i Validation.SizeBetween(nameof(hash), hash.Length, MinHashSize, MaxHashSize); Validation.NotLessThanMin(nameof(iterations), iterations, MinIterations); Validation.NotLessThanMin(nameof(memorySize), memorySize, MinMemorySize); + ThrowIfInvalidHashPrefix(hash); Sodium.Initialize(); fixed (byte* h = hash) { int ret = crypto_pwhash_str_needs_rehash(h, (ulong)iterations, (nuint)memorySize); - return ret == -1 ? throw new FormatException("Invalid password hash.") : ret == 1; + return ret == -1 ? throw new FormatException("Invalid encoded password hash.") : ret == 1; } } + + private static void ThrowIfInvalidHashPrefix(ReadOnlySpan hash) + { + if (!ConstantTime.Equals(hash[..HashPrefix.Length], Encoding.UTF8.GetBytes(HashPrefix))) + throw new FormatException("Invalid encoded password hash prefix."); + } } diff --git a/src/Geralt/Interop/Interop.Argon2id.cs b/src/Geralt/Interop/Interop.Argon2id.cs index 0295320..463c7ef 100644 --- a/src/Geralt/Interop/Interop.Argon2id.cs +++ b/src/Geralt/Interop/Interop.Argon2id.cs @@ -4,16 +4,13 @@ internal static partial class Interop { internal static partial class Libsodium { - internal const int crypto_pwhash_argon2i_ALG_ARGON2I13 = 1; internal const int crypto_pwhash_argon2id_ALG_ARGON2ID13 = 2; + internal const int crypto_pwhash_SALTBYTES = 16; internal const int crypto_pwhash_BYTES_MIN = 16; internal const int crypto_pwhash_STRBYTES = 128; - internal const string crypto_pwhash_argon2id_STRPREFIX = "$argon2id$"; internal const int crypto_pwhash_MEMLIMIT_MIN = 8192; - internal const long crypto_pwhash_OPSLIMIT_MAX = 4294967295; internal const int crypto_pwhash_argon2id_OPSLIMIT_MIN = 1; - internal const int crypto_pwhash_argon2i_OPSLIMIT_MIN = 3; - internal const int crypto_pwhash_SALTBYTES = 16; + internal const string crypto_pwhash_argon2id_STRPREFIX = "$argon2id$"; [DllImport(DllName, CallingConvention = Convention)] internal static extern unsafe int crypto_pwhash(byte* hash, ulong hashLength, byte* password, ulong passwordLength, byte* salt, ulong iterations, nuint memorySize, int algorithm);