From baf2518fe81541e5dd0b36814602e5c95a64fc5e Mon Sep 17 00:00:00 2001 From: Garritt McCune Date: Wed, 10 Mar 2021 21:27:19 -0600 Subject: [PATCH] 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. --- .vs/SecureCore/v16/.suo | Bin 89088 -> 81408 bytes SecureCore/AppSettingsManager.cs | 47 +++++++-- SecureCore/Authentication/PasswordManager.cs | 103 ++++++++++--------- SecureCore/Startup.cs | 4 +- SecureCore/appsettings.json | 4 +- 5 files changed, 97 insertions(+), 61 deletions(-) diff --git a/.vs/SecureCore/v16/.suo b/.vs/SecureCore/v16/.suo index 3926dbea297e0e449d62c8b4426fa32e1ab21888..455cb8190247cdf140077868448b065d0e27d840 100644 GIT binary patch delta 5844 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 J&kJXq1ORr2a-jeK delta 7517 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 zurped^)3l(); 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 } }