diff --git a/.vs/SecureCore/v16/.suo b/.vs/SecureCore/v16/.suo index 3926dbe..455cb81 100644 Binary files a/.vs/SecureCore/v16/.suo and b/.vs/SecureCore/v16/.suo differ diff --git a/SecureCore/AppSettingsManager.cs b/SecureCore/AppSettingsManager.cs index 7a9e516..f2b236d 100644 --- a/SecureCore/AppSettingsManager.cs +++ b/SecureCore/AppSettingsManager.cs @@ -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(); 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(); 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(), 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; + } } } } diff --git a/SecureCore/Authentication/PasswordManager.cs b/SecureCore/Authentication/PasswordManager.cs index b4f38c4..d01ef31 100644 --- a/SecureCore/Authentication/PasswordManager.cs +++ b/SecureCore/Authentication/PasswordManager.cs @@ -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[] 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)); } } } diff --git a/SecureCore/Startup.cs b/SecureCore/Startup.cs index ea17aaf..b2f0103 100644 --- a/SecureCore/Startup.cs +++ b/SecureCore/Startup.cs @@ -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; } diff --git a/SecureCore/appsettings.json b/SecureCore/appsettings.json index 8f0fd6b..896460d 100644 --- a/SecureCore/appsettings.json +++ b/SecureCore/appsettings.json @@ -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 } }