Updated the way application settings are loaded and added some intialization functions to the various manager classes to load and validate all the user supplied options at start up instead of constantly reevaluating them over the course of the application's runtime.

This commit is contained in:
2021-03-10 21:27:19 -06:00
parent 3d3b9c1e68
commit baf2518fe8
5 changed files with 97 additions and 61 deletions
Binary file not shown.
+36 -11
View File
@@ -1,17 +1,25 @@
using System.IO;
using Microsoft.Extensions.Configuration;
using System;
using System.IO;
using Newtonsoft.Json.Linq;
namespace SecureCore
{
public static class AppSettingsManager
{
private static JObject Settings { get; set; }
private static string AppSettingsPath { get; } = Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json");
public static bool TryGetConnectionString(string connectionStringName, out string connectionString)
{
connectionString = string.Empty;
try
{
connectionString = GetConfiguration().GetConnectionString(connectionStringName);
var token = Settings.SelectToken($"ConnectionStrings.{connectionStringName}");
if (token == null) return false;
connectionString = token.ToObject<string>();
if (!string.IsNullOrEmpty(connectionString)) return true;
else return false;
@@ -25,7 +33,11 @@ namespace SecureCore
try
{
setting = GetConfiguration().GetSection(sectionName)[key];
var token = Settings.SelectToken($"{sectionName}.{key}");
if (token == null) return false;
setting = token.ToObject<string>();
if(!string.IsNullOrEmpty(setting)) return true;
else return false;
@@ -39,20 +51,33 @@ namespace SecureCore
try
{
var settingString = GetConfiguration().GetSection(sectionName)[key];
var token = Settings.SelectToken($"{sectionName}.{key}");
if (int.TryParse(settingString, out setting)) return true;
if (token == null) return false;
if (int.TryParse(token.ToObject<string>(), out setting)) return true;
else return false;
}
catch { return false; }
}
private static IConfigurationRoot GetConfiguration()
public static void InitializeSettings()
{
return new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
if (Settings != null) throw new InvalidOperationException("The in memory JSON settings has already been loaded. Operation aborted.");
if (!File.Exists(AppSettingsPath)) throw new FileNotFoundException($"The app settings file '{AppSettingsPath}' couldn't be found.");
try
{
using var reader = new StreamReader(AppSettingsPath);
Settings = JObject.Parse(reader.ReadToEnd());
reader.Close();
}
catch
{
throw;
}
}
}
}
+56 -47
View File
@@ -8,41 +8,22 @@ namespace SecureCore.Authentication
public static class PasswordManager
{
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 string SectionName { get; } = "PasswordSettings";
public static int AbsoluteMinPasswordLength { get; } = 16;
public static int AbsoluteMaxPasswordLength { get; } = 128;
private static int MinPasswordLength { get; set; }
private static int MaxPasswordLength { get; set; }
private static int Iterations { get; set; }
public static (string Hash, string Salt) HashPassword(string password)
//Application enforced absolutes that can not fall short of or be exceeded by the configuartion settings of this application.
private static int AbsoluteMinPasswordLength { get; } = 16;
private static int AbsoluteMaxPasswordLength { get; } = 128;
private static int AbsoluteMinPepperLength { get; } = 32;
private static int AbsoluteMinWorkFactor { get; set; } = 10000;
public static void InitializeSettings()
{
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 (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
@@ -62,14 +43,55 @@ namespace SecureCore.Authentication
}
else minPasswordLength = AbsoluteMinPasswordLength;
// Validate the settings further.
if(minPasswordLength == maxPasswordLength || minPasswordLength > maxPasswordLength)
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.");
MinPasswordLength = minPasswordLength;
MaxPasswordLength = maxPasswordLength;
//Now read in the pepper. A pepper being a string of characters at least 32 characters long that is NOT stored in the database and is used in conjunction with hashing sensitive user data.
if (AppSettingsManager.TryGetSettingString(SectionName, "Pepper", out string pepper))
{
//As noted here https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html a pepper should be at least 32 bytes in size.
if (pepper.Length < AbsoluteMinPepperLength) throw new Exception("A pepper must be at least 32 characters long for security reasons.");
}
else
throw new Exception($"A pepper, generated by a cryptographically secure random number generator, that is at least 32 characters long, must be supplied in the appsettings.json file (Key path: {SectionName}.Pepper).");
Pepper = pepper;
//Finally, the work factor (A.K.A. iterations) for the hashing algorithm.
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 < AbsoluteMinWorkFactor) iterations = AbsoluteMinWorkFactor;
}
//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;
Iterations = iterations;
}
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 (string.IsNullOrEmpty(password)) return (false, "No password has been supplied, and thus is not valid.");
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);
}
@@ -131,7 +153,7 @@ namespace SecureCore.Authentication
public static string HashStringData(string data, string salt = "")
{
_ = new byte[0];
_ = Array.Empty<byte>();
byte[] saltBytes;
if (!string.IsNullOrEmpty(salt))
@@ -147,20 +169,7 @@ namespace SecureCore.Authentication
private static string GetHash(string password, byte[] salt)
{
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));
return Convert.ToBase64String(KeyDerivation.Pbkdf2($"{password}{Pepper}", salt, KeyType, Iterations, KeySize));
}
}
}
+3 -1
View File
@@ -5,7 +5,6 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.HttpOverrides;
using System.Net;
using SecureCore.Services;
namespace SecureCore
@@ -14,6 +13,9 @@ namespace SecureCore
{
public Startup(IConfiguration configuration)
{
AppSettingsManager.InitializeSettings();
Authentication.PasswordManager.InitializeSettings();
Configuration = configuration;
}
+2 -2
View File
@@ -14,8 +14,8 @@
"doamin": "copyrightcrusader.org"
},
"PasswordSettings": {
"Pepper": "rVk/OwQUw01qy76Q+5WimPk+NdqUMMghftMXyJzzckOj/+eFn056PDYzBD61E/ZNjRdgiMK6RhcHEcdfpJdbcw==",
"Peppser": "rVk/OwQUw01qy76Q+5WimPk+NdqUMMghftMXyJzzckOj/+eFn056PDYzBD61E/ZNjRdgiMK6RhcHEcdfpJdbcw==",
"MaxLength": 128,
"MinLength": 17
"MinLength": 22
}
}