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:
Binary file not shown.
@@ -1,17 +1,25 @@
|
|||||||
using System.IO;
|
using System;
|
||||||
using Microsoft.Extensions.Configuration;
|
using System.IO;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace SecureCore
|
namespace SecureCore
|
||||||
{
|
{
|
||||||
public static class AppSettingsManager
|
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)
|
public static bool TryGetConnectionString(string connectionStringName, out string connectionString)
|
||||||
{
|
{
|
||||||
connectionString = string.Empty;
|
connectionString = string.Empty;
|
||||||
|
|
||||||
try
|
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;
|
if (!string.IsNullOrEmpty(connectionString)) return true;
|
||||||
else return false;
|
else return false;
|
||||||
@@ -25,7 +33,11 @@ namespace SecureCore
|
|||||||
|
|
||||||
try
|
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;
|
if(!string.IsNullOrEmpty(setting)) return true;
|
||||||
else return false;
|
else return false;
|
||||||
@@ -39,20 +51,33 @@ namespace SecureCore
|
|||||||
|
|
||||||
try
|
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;
|
else return false;
|
||||||
}
|
}
|
||||||
catch { return false; }
|
catch { return false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IConfigurationRoot GetConfiguration()
|
public static void InitializeSettings()
|
||||||
{
|
{
|
||||||
return new ConfigurationBuilder()
|
if (Settings != null) throw new InvalidOperationException("The in memory JSON settings has already been loaded. Operation aborted.");
|
||||||
.SetBasePath(Directory.GetCurrentDirectory())
|
if (!File.Exists(AppSettingsPath)) throw new FileNotFoundException($"The app settings file '{AppSettingsPath}' couldn't be found.");
|
||||||
.AddJsonFile("appsettings.json")
|
|
||||||
.Build();
|
try
|
||||||
|
{
|
||||||
|
using var reader = new StreamReader(AppSettingsPath);
|
||||||
|
|
||||||
|
Settings = JObject.Parse(reader.ReadToEnd());
|
||||||
|
|
||||||
|
reader.Close();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,41 +8,22 @@ namespace SecureCore.Authentication
|
|||||||
public static class PasswordManager
|
public static class PasswordManager
|
||||||
{
|
{
|
||||||
private static string Pepper { get; set; }
|
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 KeyDerivationPrf KeyType { get; } = KeyDerivationPrf.HMACSHA512;
|
||||||
private static int KeySize { get; } = 512 / 8;
|
private static int KeySize { get; } = 512 / 8;
|
||||||
private static int SaltSize { get; } = 128 / 8; //128 bit salt
|
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 string SectionName { get; } = "PasswordSettings";
|
||||||
public static int AbsoluteMinPasswordLength { get; } = 16;
|
private static int MinPasswordLength { get; set; }
|
||||||
public static int AbsoluteMaxPasswordLength { get; } = 128;
|
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))
|
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
|
//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;
|
else minPasswordLength = AbsoluteMinPasswordLength;
|
||||||
// Validate the settings further.
|
// Validate the settings further.
|
||||||
if(minPasswordLength == maxPasswordLength || minPasswordLength > maxPasswordLength)
|
if (minPasswordLength == maxPasswordLength || minPasswordLength > maxPasswordLength)
|
||||||
{
|
{
|
||||||
minPasswordLength = AbsoluteMinPasswordLength;
|
minPasswordLength = AbsoluteMinPasswordLength;
|
||||||
maxPasswordLength = AbsoluteMaxPasswordLength;
|
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.");
|
MinPasswordLength = minPasswordLength;
|
||||||
if (password.Length > maxPasswordLength) return (false, $"Your password is too long, it must not exceed {maxPasswordLength} characters and must contain at least {minPasswordLength} characters.");
|
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);
|
return (true, string.Empty);
|
||||||
}
|
}
|
||||||
@@ -131,7 +153,7 @@ namespace SecureCore.Authentication
|
|||||||
|
|
||||||
public static string HashStringData(string data, string salt = "")
|
public static string HashStringData(string data, string salt = "")
|
||||||
{
|
{
|
||||||
_ = new byte[0];
|
_ = Array.Empty<byte>();
|
||||||
byte[] saltBytes;
|
byte[] saltBytes;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(salt))
|
if (!string.IsNullOrEmpty(salt))
|
||||||
@@ -147,20 +169,7 @@ namespace SecureCore.Authentication
|
|||||||
|
|
||||||
private static string GetHash(string password, byte[] salt)
|
private static string GetHash(string password, byte[] salt)
|
||||||
{
|
{
|
||||||
if (AppSettingsManager.TryGetSettingInt(SectionName, "Iterations", out int iterations))
|
return Convert.ToBase64String(KeyDerivation.Pbkdf2($"{password}{Pepper}", salt, KeyType, Iterations, KeySize));
|
||||||
{
|
|
||||||
//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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ using Microsoft.Extensions.Configuration;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using System.Net;
|
|
||||||
using SecureCore.Services;
|
using SecureCore.Services;
|
||||||
|
|
||||||
namespace SecureCore
|
namespace SecureCore
|
||||||
@@ -14,6 +13,9 @@ namespace SecureCore
|
|||||||
{
|
{
|
||||||
public Startup(IConfiguration configuration)
|
public Startup(IConfiguration configuration)
|
||||||
{
|
{
|
||||||
|
AppSettingsManager.InitializeSettings();
|
||||||
|
Authentication.PasswordManager.InitializeSettings();
|
||||||
|
|
||||||
Configuration = configuration;
|
Configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
"doamin": "copyrightcrusader.org"
|
"doamin": "copyrightcrusader.org"
|
||||||
},
|
},
|
||||||
"PasswordSettings": {
|
"PasswordSettings": {
|
||||||
"Pepper": "rVk/OwQUw01qy76Q+5WimPk+NdqUMMghftMXyJzzckOj/+eFn056PDYzBD61E/ZNjRdgiMK6RhcHEcdfpJdbcw==",
|
"Peppser": "rVk/OwQUw01qy76Q+5WimPk+NdqUMMghftMXyJzzckOj/+eFn056PDYzBD61E/ZNjRdgiMK6RhcHEcdfpJdbcw==",
|
||||||
"MaxLength": 128,
|
"MaxLength": 128,
|
||||||
"MinLength": 17
|
"MinLength": 22
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user