using System; using System.Data.SqlClient; using Microsoft.AspNetCore.Cryptography.KeyDerivation; namespace SecureCore.Authentication { public 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 string ConnectionString = @"Server=DESKTOP-OEDDVKC\SQLEXPRESS;Database=main;Integrated Security=true;"; 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 PasswordManager() { } 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) PasswordIsValid(string password, string salt, string passwordHash) { 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."); var saltBytes = Convert.FromBase64String(salt); if (passwordHash == GetHash(password, saltBytes)) return (true, string.Empty); else return (false, string.Empty); } public static (string PasswordHash, string SaltHash) GetUserPasswordHash(int userId) { using (var connection = new SqlConnection(ConnectionString)) { using (var command = new SqlCommand("SELECT [Password Hash], [Salt Hash] FROM Login WHERE [User Key] = @UserId", connection)) { command.Parameters.AddWithValue("UserId", userId); connection.Open(); var reader = command.ExecuteReader(); if (!reader.HasRows) throw new MissingFieldException("No login records exist for this user."); reader.Read(); return (reader["Password Hash"].ToString(), reader["Salt Hash"].ToString()); } } } public static string HashStringData(string data, string salt = "") { var saltBytes = new byte[0]; 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)); } } }