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:
2021-03-09 22:14:29 -06:00
parent 70bb9dd9e2
commit 7008621799
53 changed files with 157 additions and 4377 deletions
+59 -9
View File
@@ -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));
}
}
}
+1 -1
View File
@@ -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;