Files
secure-core/SecureCore/Authentication/PasswordManager.cs
T

119 lines
5.0 KiB
C#

using System;
using System.Data;
using System.Data.SqlClient;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
namespace SecureCore.Authentication
{
public static class PasswordManager
{
//Add some pepper to the passwords for good measure:
//https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
private static string Pepper { get; } = "rVk/OwQUw01qy76Q+5WimPk+NdqUMMghftMXyJzzckOj/+eFn056PDYzBD61E/ZNjRdgiMK6RhcHEcdfpJdbcw==";
private static int Iterations { get; } = 100000;
private static KeyDerivationPrf KeyType { get; } = KeyDerivationPrf.HMACSHA512;
private static int KeySize { get; } = 512 / 8;
private static int SaltSize { get; } = 128 / 8; //128 bit salt
//As noted here: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#maximum-password-lengths
//allowing passwords that are too long can result in a denial-of-service attack. So we must enforce password length limits.
//The recommended length is between 64 and 128, so I decided to go for the upper bounds.
public static int MaxPasswordLength { get; } = 128;
public static int MinPasswordLength { get; } = 16;
public static (string Hash, string Salt) HashPassword(string password)
{
var salt = new byte[SaltSize];
ByteGenerator.GetRandomBytes(ref salt);
return (GetHash(password, salt), Convert.ToBase64String(salt));
}
public static (bool IsValid, string Message) IsPasswordValid(string password)
{
if (password.Length < MinPasswordLength) return (false, $"Your password is too short, it must be at least {MinPasswordLength} characters long and not exceed {MaxPasswordLength} characters.");
if (password.Length > MaxPasswordLength) return (false, $"Your password is too long, it must not exceed {MaxPasswordLength} characters and must contain at least {MinPasswordLength} characters.");
return (true, string.Empty);
}
public static bool IsPasswordAMatch(string password, string salt, string passwordHash)
{
var saltBytes = Convert.FromBase64String(salt);
return passwordHash == GetHash(password, saltBytes);
}
public static void InsertPasswordResetRequest(string email, string sessionToken, DateTime expirationDate, string userAgent, string ipAddress, string connectionString)
{
using var connection = new SqlConnection(connectionString);
using var command = new SqlCommand("LogPasswordResetRequest", connection) { CommandType = CommandType.StoredProcedure };
command.Parameters.AddWithValue("SessionToken", sessionToken);
command.Parameters.AddWithValue("ExpirationDate", expirationDate);
command.Parameters.AddWithValue("Email", email);
command.Parameters.AddWithValue("UserAgent", userAgent);
command.Parameters.AddWithValue("IpAddress", ipAddress);
connection.Open();
command.ExecuteNonQuery();
}
public static void ResetPassword(string passwordHash, string salt, string sessionToken, string connectionString)
{
using var connection = new SqlConnection(connectionString);
using var command = new SqlCommand("ResetPassword", connection) { CommandType = CommandType.StoredProcedure };
command.Parameters.AddWithValue("PasswordHash", passwordHash);
command.Parameters.AddWithValue("Salt", salt);
command.Parameters.AddWithValue("SessionToken", sessionToken);
connection.Open();
command.ExecuteNonQuery();
}
public static (string PasswordHash, string Salt) GetPasswordHashAndSalt(string username, string connectionString)
{
using var connection = new SqlConnection(connectionString);
using var command = new SqlCommand("SELECT [Password Hash], [Salt] FROM dbo.GetUserPasswordHashAndSalt(@Username)", connection);
command.Parameters.AddWithValue("Username", username);
connection.Open();
var reader = command.ExecuteReader();
reader.Read();
if (!reader.HasRows) return (string.Empty, string.Empty);
return (reader["Password Hash"].ToString(), reader["Salt"].ToString());
}
public static string HashStringData(string data, string salt = "")
{
_ = new byte[0];
byte[] saltBytes;
if (!string.IsNullOrEmpty(salt))
saltBytes = Convert.FromBase64String(salt);
else
{
saltBytes = new byte[SaltSize];
ByteGenerator.GetRandomBytes(ref saltBytes);
}
return GetHash(data, saltBytes);
}
private static string GetHash(string password, byte[] salt)
{
return Convert.ToBase64String(KeyDerivation.Pbkdf2($"{password}{Pepper}", salt, KeyType, Iterations, KeySize));
}
}
}