Added code to support password settings. Fixed a bug where the [FromBody] attribute resulted in null object properties, as well as included more null checks.
This commit is contained in:
@@ -7,18 +7,28 @@ 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 string Pepper { get; set; }
|
||||
public static string PasswordPepper
|
||||
{
|
||||
get { return Pepper; }
|
||||
set
|
||||
{
|
||||
//As noted here https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html a pepper should be at least
|
||||
//32 bytes in size.
|
||||
if (value.Length < 32) throw new Exception("A pepper must be at least 32 characters long for security reasons.");
|
||||
if (string.IsNullOrEmpty(Pepper)) Pepper = value;
|
||||
else throw new InvalidOperationException("Pepper can only be set at the startup of the application.");
|
||||
}
|
||||
}
|
||||
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 SectionName { get; } = "PasswordSettings";
|
||||
private static int AbsoluteMinPasswordLength { get; } = 16;
|
||||
private static int AbsoluteMaxPasswordLength { get; } = 128;
|
||||
|
||||
public static (string Hash, string Salt) HashPassword(string password)
|
||||
{
|
||||
@@ -31,8 +41,35 @@ namespace SecureCore.Authentication
|
||||
|
||||
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.");
|
||||
if (string.IsNullOrEmpty(password)) return (false, "No password has been supplied, and thus is not valid.");
|
||||
|
||||
if (AppSettingsManager.TryGetSettingInt(SectionName, "MaxLength", out int maxPasswordLength))
|
||||
{
|
||||
//As noted in this article 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 an upper bound
|
||||
//on the length of passwords. The recommended length is between 64 and 128, so if the length is set long than
|
||||
//128 characters, just default to 128 as that's a safe upper limit.
|
||||
if (maxPasswordLength > AbsoluteMaxPasswordLength || maxPasswordLength < AbsoluteMinPasswordLength) maxPasswordLength = AbsoluteMaxPasswordLength;
|
||||
}
|
||||
else maxPasswordLength = AbsoluteMaxPasswordLength;
|
||||
|
||||
if (AppSettingsManager.TryGetSettingInt(SectionName, "MinLength", out int minPasswordLength))
|
||||
{
|
||||
//There was no mention of a min password length in the above article, so I've chosen on a whim that 16 should
|
||||
//be a safe enough min on a password's length. So as usual, just ignore settings that are out of bounds and
|
||||
//instead stick to safe values that are in bounds.
|
||||
if (minPasswordLength < AbsoluteMinPasswordLength || minPasswordLength > AbsoluteMaxPasswordLength) minPasswordLength = AbsoluteMinPasswordLength;
|
||||
}
|
||||
else minPasswordLength = AbsoluteMinPasswordLength;
|
||||
// Validate the settings further.
|
||||
if(minPasswordLength == maxPasswordLength || minPasswordLength > maxPasswordLength)
|
||||
{
|
||||
minPasswordLength = AbsoluteMinPasswordLength;
|
||||
maxPasswordLength = AbsoluteMaxPasswordLength;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -110,7 +147,20 @@ namespace SecureCore.Authentication
|
||||
|
||||
private static string GetHash(string password, byte[] salt)
|
||||
{
|
||||
return Convert.ToBase64String(KeyDerivation.Pbkdf2($"{password}{Pepper}", salt, KeyType, Iterations, KeySize));
|
||||
if (AppSettingsManager.TryGetSettingInt(SectionName, "Iterations", out int iterations))
|
||||
{
|
||||
//The work factor must be of a certain strength and if it fails this check then we will be forced to ignore it and use the recommended work factor
|
||||
//as stated here: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2
|
||||
//As stated in the above link, 10,000 iterations is the lowest we should ever go. So for security's sake, that's the lower bounds that will be allowed.
|
||||
if (iterations < 10000) iterations = 10000;
|
||||
}
|
||||
//By default, we'll go with the highest security setting if one isn't provided by an admin.
|
||||
//To quote the above link:
|
||||
//"The work factor for PBKDF2 is implemented through the iteration count, which should be at least 10,000
|
||||
//(although values of up to 100,000 may be appropriate in higher security environments)."
|
||||
else iterations = 100000;
|
||||
|
||||
return Convert.ToBase64String(KeyDerivation.Pbkdf2($"{password}{PasswordPepper}", salt, KeyType, iterations, KeySize));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace SecureCore.Authentication
|
||||
|
||||
public static bool IsSessionTokenValid(HttpContext context)
|
||||
{
|
||||
AppSettingsManager.TryGetConnectionStringByName("MainDataConnectionString", out string connectionString);
|
||||
AppSettingsManager.TryGetConnectionString("MainDataConnectionString", out string connectionString);
|
||||
|
||||
if (!context.Request.Cookies.ContainsKey(SessionCookieName)) return false;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user