Issue with unit test for Argon2-based password hash service

I'm encountering an issue with the implementation of a password hash service using the Argon2 algorithm. I've created a PasswordHasherService class that utilizes Argon2 to generate secure password hashes, along with a method to verify passwords. The implementation of the class seems to be correct, but I'm facing difficulties with a specific unit test. The unit test aims to verify if the VerifyPassword() method returns false when an incorrect salt is provided. Here's an overview of the problem: The PasswordHasherService class has three main methods: HashPassword(): generates a secure password hash using Argon2 and returns the hash along with the salt used. VerifyPassword(): checks if a provided password matches a provided password hash, taking into account the salt used during hash generation. GenerateSalt(): generates a random salt. The unit test in question, named VerifyPassword_ReturnsFalseForIncorrectSalt, checks if the VerifyPassword() method returns false when an incorrect salt is provided. However, even when intentionally providing an incorrect salt in the unit test, the returned result is true, indicating that the test is failing. I've reviewed the code multiple times and ensured that the implementation of the PasswordHasherService class is correct. I've also confirmed that the salt is being generated correctly using the GenerateSalt() method. However, I haven't been able to pinpoint the root cause of this issue. I'm reaching out to the community for assistance in understanding what might be causing this unexpected behavior and how I can fix my unit test to ensure it functions as expected. Any guidance or suggestions would be greatly appreciated. Thank you in advance for your help!
21 Replies
KaykiLetieri.cs
KaykiLetieri.cs2mo ago
using Isopoh.Cryptography.Argon2;
using RegulatorioAuth.Application.Services.Interfaces;
using System.Security.Cryptography;
using System.Text;

namespace RegulatorioAuth.Application.Services;

public class PasswordHasherService : IPasswordHasherService
{
public (string HashedPassword, byte[] Salt) HashPassword(string password)
{
var salt = GenerateSalt();

Argon2Config argon2Config = new()
{
Type = Argon2Type.DataIndependentAddressing,
Version = Argon2Version.Nineteen,
MemoryCost = 65536,
TimeCost = 4,
Lanes = 8,
Threads = 1,
Password = Encoding.UTF8.GetBytes(password),
Salt = salt
};

using Argon2 argon2 = new(argon2Config);
using var hash = argon2.Hash();

return (argon2Config.EncodeString(hash.Buffer), salt);
}

public bool VerifyPassword(string password, string hashedPassword, byte[] salt)
{
Argon2Config configOfPasswordToVerify = new Argon2Config
{
Type = Argon2Type.DataIndependentAddressing,
Version = Argon2Version.Nineteen,
MemoryCost = 65536,
TimeCost = 4,
Lanes = 8,
Threads = 1,
Password = Encoding.UTF8.GetBytes(password),
Salt = salt
};

return Argon2.Verify(hashedPassword, configOfPasswordToVerify);
}

public byte[] GenerateSalt()
{
byte[] salt = new byte[16];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(salt);
}
return salt;
}
}
using Isopoh.Cryptography.Argon2;
using RegulatorioAuth.Application.Services.Interfaces;
using System.Security.Cryptography;
using System.Text;

namespace RegulatorioAuth.Application.Services;

public class PasswordHasherService : IPasswordHasherService
{
public (string HashedPassword, byte[] Salt) HashPassword(string password)
{
var salt = GenerateSalt();

Argon2Config argon2Config = new()
{
Type = Argon2Type.DataIndependentAddressing,
Version = Argon2Version.Nineteen,
MemoryCost = 65536,
TimeCost = 4,
Lanes = 8,
Threads = 1,
Password = Encoding.UTF8.GetBytes(password),
Salt = salt
};

using Argon2 argon2 = new(argon2Config);
using var hash = argon2.Hash();

return (argon2Config.EncodeString(hash.Buffer), salt);
}

public bool VerifyPassword(string password, string hashedPassword, byte[] salt)
{
Argon2Config configOfPasswordToVerify = new Argon2Config
{
Type = Argon2Type.DataIndependentAddressing,
Version = Argon2Version.Nineteen,
MemoryCost = 65536,
TimeCost = 4,
Lanes = 8,
Threads = 1,
Password = Encoding.UTF8.GetBytes(password),
Salt = salt
};

return Argon2.Verify(hashedPassword, configOfPasswordToVerify);
}

public byte[] GenerateSalt()
{
byte[] salt = new byte[16];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(salt);
}
return salt;
}
}
[Fact]
public void VerifyPassword_ReturnsFalseForIncorrectSalt()
{
// Arrange
string password = "password123";
var (hashedPassword, _) = _passwordHasherService.HashPassword(password);
byte[] incorrectSalt = Encoding.UTF8.GetBytes("WrongSalt");

// Act
bool result = _passwordHasherService.VerifyPassword(password, hashedPassword, incorrectSalt);

// Assert
Assert.False(result);
}
[Fact]
public void VerifyPassword_ReturnsFalseForIncorrectSalt()
{
// Arrange
string password = "password123";
var (hashedPassword, _) = _passwordHasherService.HashPassword(password);
byte[] incorrectSalt = Encoding.UTF8.GetBytes("WrongSalt");

// Act
bool result = _passwordHasherService.VerifyPassword(password, hashedPassword, incorrectSalt);

// Assert
Assert.False(result);
}
Assert.False() Failure
Expected: False
Actual: True
Assert.False() Failure
Expected: False
Actual: True
qqdev
qqdev2mo ago
So VerifyPassword returns true for the wrong salt
KaykiLetieri.cs
KaykiLetieri.cs2mo ago
yep
qqdev
qqdev2mo ago
That's interesting
KaykiLetieri.cs
KaykiLetieri.cs2mo ago
It could be an error with the Isopoh.Cryptography.Argon2 library?
qqdev
qqdev2mo ago
Maybe. The usage looks fine to me
qqdev
qqdev2mo ago
No description
qqdev
qqdev2mo ago
Might wanna try this one if you can: https://github.com/kmaragon/Konscious.Security.Cryptography E: Has its own issues
qqdev
qqdev2mo ago
GitHub
Hash without salt is corrupt · Issue #52 · mheyman/Isopoh.Cryptogra...
Seems kind of basic, but the hash is corrupt if a salt isn't supplied. Argon2.Hash(new Argon2Config { Password = Encoding.UTF8.GetBytes("test") }) // $argon2id$v=19$m=65536,t=3,p=4 Ar...
qqdev
qqdev2mo ago
Stuff like that makes me worry ;D
KaykiLetieri.cs
KaykiLetieri.cs2mo ago
really I'm going to switch thank you very much
qqdev
qqdev2mo ago
Do you have to use argon2?
qqdev
qqdev2mo ago
Stack Overflow
Hash and salt passwords in C#
I was just going through one of DavidHayden's articles on Hashing User Passwords. Really I can't get what he is trying to achieve. Here is his code: private static string CreateSalt(int size) { ...
KaykiLetieri.cs
KaykiLetieri.cs2mo ago
Is it better than using Argon2?
qqdev
qqdev2mo ago
Google "argon2 vs PBKDF2" for more info on that
qqdev
qqdev2mo ago
GitHub
CheatSheetSeries/cheatsheets/Password_Storage_Cheat_Sheet.md at mas...
The OWASP Cheat Sheet Series was created to provide a concise collection of high value information on specific application security topics. - OWASP/CheatSheetSeries
KaykiLetieri.cs
KaykiLetieri.cs2mo ago
Thank you very much I'll check it out
Lisa
Lisa2mo ago
This is because you are using argon2Config.EncodeString EncodeString includes information about how the hash was computed, including the salt. In this case the provided salt is ignored and the salt from the hashstring is used.
Lisa
Lisa2mo ago
// See https://aka.ms/new-console-template for more information

using System.Security.Cryptography;
using Isopoh.Cryptography.Argon2;

var salt = new byte[16];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(salt);
}

Console.WriteLine(salt.ToB64String());

Argon2Config argon2Config = new()
{
Type = Argon2Type.DataIndependentAddressing,
Version = Argon2Version.Nineteen,
MemoryCost = 65536,
TimeCost = 4,
Lanes = 8,
Threads = 1,
Password = "somepassword"u8.ToArray(),
Salt = salt
};

var argon2 = new Argon2(argon2Config);

var hash = argon2Config.EncodeString(argon2.Hash().Buffer);
Console.WriteLine(hash);
// See https://aka.ms/new-console-template for more information

using System.Security.Cryptography;
using Isopoh.Cryptography.Argon2;

var salt = new byte[16];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(salt);
}

Console.WriteLine(salt.ToB64String());

Argon2Config argon2Config = new()
{
Type = Argon2Type.DataIndependentAddressing,
Version = Argon2Version.Nineteen,
MemoryCost = 65536,
TimeCost = 4,
Lanes = 8,
Threads = 1,
Password = "somepassword"u8.ToArray(),
Salt = salt
};

var argon2 = new Argon2(argon2Config);

var hash = argon2Config.EncodeString(argon2.Hash().Buffer);
Console.WriteLine(hash);
No description
Lisa
Lisa2mo ago
As you can see the salt is included in plaintext in the encoded string.
KaykiLetieri.cs
KaykiLetieri.cs2mo ago
Thanks