Жесткий код C# свойство байта в модель - PullRequest
2 голосов
/ 07 февраля 2020

Я пишу xunit для проверки Authenticate метода. Это довольно просто:

public User Authenticate(string username, string password)
{
        if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
            return null;

        var user = _context.Users.SingleOrDefault(x => x.Username == username);

        // check if username exists
        if (user == null)
            return null;

        // check if password is correct
        if (!VerifyPasswordHash(password, user.PasswordHash, user.PasswordSalt))
            return null;

        // authentication successful
        return user;
}

VerifyPasswordHash метод:

 private static bool VerifyPasswordHash(string password, byte[] storedHash, byte[] storedSalt)
    {
        if (password == null) throw new ArgumentNullException("password");
        if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be empty or whitespace only string.", "password");
        if (storedHash.Length != 64) throw new ArgumentException("Invalid length of password hash (64 bytes expected).", "passwordHash");
        if (storedSalt.Length != 128) throw new ArgumentException("Invalid length of password salt (128 bytes expected).", "passwordHash");

        using (var hmac = new System.Security.Cryptography.HMACSHA512(storedSalt))
        {
            var computedHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
            for (int i = 0; i < computedHash.Length; i++)
            {
                if (computedHash[i] != storedHash[i]) return false;
            }
        }

        return true;
    }

Но чтобы проверить это, мне нужно заполнить мою БД несколькими User сущностями.

Вот что я пытался сделать:

public void TestAuthenticate()
{
        //Arrange
        var options = new DbContextOptionsBuilder<DataContext>() //instead of mocking we use inMemoryDatabase.
            .UseInMemoryDatabase(databaseName: "TestAuthenticate")
            .Options;

        var config = new MapperConfiguration(cfg =>
            cfg.AddProfile<AutoMapperProfile>());

        var mapper = config.CreateMapper();
        var fakeUser = new User()
        {
            Username = "anon1", FirstName = "fakename", LastName = "fakelastname", Role = "admin", PasswordHash = null, PasswordSalt = null
        };

        using (var context = new DataContext(options))
        {
            context.Users.Add(fakeUser);
            context.SaveChanges();
        }

        // Act
        using (var context = new DataContext(options))
        {
            var service = new UserService(context, mapper);
            var result = service.Authenticate(fakeUser.Username, "somepassword");

            // Assert
            Assert.IsType<User>(result);
        }
}

Я сделал PasswordHash и PasswordSalt здесь нулем, но они должны быть байтами [], вот как они хранятся в базе данных:

public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Username { get; set; }
    public byte[] PasswordHash { get; set; }
    public byte[] PasswordSalt { get; set; }
    public string Role { get; set; }
}

Пожалуйста, дайте мне знать, как заставить этот тест работать, и оставьте отзыв, если вы находите общий тестовый лог c странным. Это моя первая попытка написания юнит-тестов.

1 Ответ

2 голосов
/ 07 февраля 2020

Я бы выделил код для создания значений ha sh в его собственный метод, который вы можете отдельно протестировать.

Так вот:

private static bool VerifyPasswordHash(string password, byte[] storedHash, byte[] storedSalt)
{
    if (password == null) throw new ArgumentNullException("password");
    if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be empty or whitespace only string.", "password");
    if (storedHash.Length != 64) throw new ArgumentException("Invalid length of password hash (64 bytes expected).", "passwordHash");
    if (storedSalt.Length != 128) throw new ArgumentException("Invalid length of password salt (128 bytes expected).", "passwordHash");

    using (var hmac = new System.Security.Cryptography.HMACSHA512(storedSalt))
    {
        var computedHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
        for (int i = 0; i < computedHash.Length; i++)
        {
            if (computedHash[i] != storedHash[i]) return false;
        }
    }

    return true;
}

Получается так:

private static byte[] ComputeHash(string data, byte[] salt)
{
    using (var hmac = new System.Security.Cryptography.HMACSHA512(salt))
    {
        return hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(data));
    }
}

private static bool VerifyPasswordHash(string password, byte[] storedHash, byte[] storedSalt)
{
    if (password == null) throw new ArgumentNullException("password");
    if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be empty or whitespace only string.", "password");
    if (storedHash.Length != 64) throw new ArgumentException("Invalid length of password hash (64 bytes expected).", "passwordHash");
    if (storedSalt.Length != 128) throw new ArgumentException("Invalid length of password salt (128 bytes expected).", "passwordHash");

    var computedHash = ComputeHash(password, storedSalt);  
    for (int i = 0; i < computedHash.Length; i++)
    {
        if (computedHash[i] != storedHash[i]) return false;
    }
    return true;
}

Для этого есть несколько целей: он позволяет вам совместно использовать этот метод с кодом для генерации хэшей паролей при создании, изменении и сбросе, делая уверенным код, изначально равный ha sh пароли используют тот же процесс, что и код для проверки хэшей; позволяет выделить поколение ha sh для отдельного модульного теста; и это делает его немного более безопасным и простым в настройке алгоритма хеширования, если sha512 перестает быть жизнеспособным. Для этого есть и другие причины.

Пока я здесь, я мог бы также добавить поле authType для пользователя, что облегчит и сделает более безопасным настройку этого алгоритма, если sha512 когда-либо перестанет быть жизнеспособным, и даже будет иметь два разных процесса активный в то же время. Например, вам может потребоваться отдельный процесс, если вам когда-либо понадобится интегрироваться с внешними службами OAuth или SAML.

Если у вас есть функция ComputeHash(), вы должны сделать нечто подобное, чтобы создать функцию GenerateRandomSalt() для вызывать при создании новых пользователей. С обоими из них создание справочных данных для вашего модульного теста полной аутентификации становится намного проще:

var fakeUser = new User()
{
    Username = "anon1", FirstName = "fakename", LastName = "fakelastname",
    Role = "admin", PasswordHash = null, PasswordSalt = GenerateRandomSalt()
};
fakeUser.PasswordHash = ComputeHash("somepassword", fakeUser.PasswordSalt);

using (var context = new DataContext(options))
{
    context.Users.Add(fakeUser);
    context.SaveChanges();
}
...