diff --git a/.vs/SecureCore/v16/.suo b/.vs/SecureCore/v16/.suo index d785088..342ce73 100644 Binary files a/.vs/SecureCore/v16/.suo and b/.vs/SecureCore/v16/.suo differ diff --git a/SecureCore/Authentication/PasswordManager.cs b/SecureCore/Authentication/PasswordManager.cs index e54d7f1..702a4a2 100644 --- a/SecureCore/Authentication/PasswordManager.cs +++ b/SecureCore/Authentication/PasswordManager.cs @@ -16,6 +16,10 @@ namespace SecureCore.Authentication 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 PasswordManager() { @@ -30,11 +34,16 @@ namespace SecureCore.Authentication return (GetHash(password, salt), Convert.ToBase64String(salt)); } - public static bool PasswordIsValid(string password, string salt, string passwordHash) + public static (bool IsValid, string Message) PasswordIsValid(string password, string salt, string passwordHash) { + if (password.Length > MaxPasswordLength) return (false, $"Password length exceeds {MaxPasswordLength} characters."); + var saltBytes = Convert.FromBase64String(salt); - return passwordHash == GetHash(password, saltBytes); + if (passwordHash == GetHash(password, saltBytes)) + return (true, string.Empty); + else + return (false, string.Empty); } public static (string PasswordHash, string SaltHash) GetUserPasswordHash(int userId) diff --git a/SecureCore/Controllers/AuthController.cs b/SecureCore/Controllers/AuthController.cs index 478974c..fbbd3b0 100644 --- a/SecureCore/Controllers/AuthController.cs +++ b/SecureCore/Controllers/AuthController.cs @@ -14,6 +14,7 @@ namespace SecureCore.Controllers [ApiController] public class AuthController : Controller { + //TODO: Login will only ever return messages like "Wrong username / password." whereas register can return messages like "User exists.", "Password to weak", or "Password in top 100 most used.". [HttpPost("login")] [AcceptVerbs("POST")] public IActionResult Login([FromBody] LoginInfo info) @@ -21,6 +22,11 @@ namespace SecureCore.Controllers //NOTE: password length should be at most 64 - 128 characters long. //Very the user has login data. //if (!UserDataService.UserHasLoginData(info.UserName)) return Unauthorized("User doesn't have login creds"); + if (info.Password.Length > PasswordManager.MaxPasswordLength) return Unauthorized($"Password exceeds maxium length of {PasswordManager.MaxPasswordLength} characters."); + + var result = UserDataService.UserNameIsValid(info.UserName); + + if (!result.IsValid) return Unauthorized(result.Message); if (HttpContext.Request.Cookies.ContainsKey("Session")) { @@ -31,8 +37,9 @@ namespace SecureCore.Controllers var (password, salt) = UserDataService.GetUserPasswordHash(info.UserName); var agent = HttpContext.Request.Headers[Microsoft.Net.Http.Headers.HeaderNames.UserAgent]; var ip = PasswordManager.HashStringData(Request.HttpContext.Connection.RemoteIpAddress.MapToIPv4().ToString(), salt); + result = PasswordManager.PasswordIsValid(info.Password, salt, password); - if (PasswordManager.PasswordIsValid(info.Password, salt, password)) + if (result.IsValid) { var session = new SessionManager(); @@ -89,14 +96,21 @@ namespace SecureCore.Controllers //[AcceptVerbs("GET")] public IActionResult ResetPassword([FromQuery] string token) { - if (!UserDataService.IsResetTokenValid(token)) return Unauthorized("Token invalid"); + try + { + if (!UserDataService.IsResetTokenValid(token)) return Unauthorized("Token invalid"); + } + catch (Exception e) + { + + } return Ok("Done"); } [HttpPost("CreatePasswordResetLink")] [AcceptVerbs("POST")] - public IActionResult CreatePasswordResetLink([FromBody] string userName) + public IActionResult CreatePasswordResetLink([FromBody] string userName) //TODO: this sig should only accept an email, so the link can be sent there. { var sessionManager = new SessionManager(); var token = sessionManager.CreateSessionToken(); diff --git a/SecureCore/Services/UserDataService.cs b/SecureCore/Services/UserDataService.cs index 8e15c01..3a54b1e 100644 --- a/SecureCore/Services/UserDataService.cs +++ b/SecureCore/Services/UserDataService.cs @@ -10,8 +10,27 @@ namespace SecureCore.Services { public static class UserDataService { + // + public static int UserNameMaxLength { get; } = 64; + private static string ConnectionString = @"Server=DESKTOP-OEDDVKC\SQLEXPRESS;Database=main;Integrated Security=true;"; + public static (bool IsValid, string Message) UserNameIsValid(string userName) + { + if (userName.Length > UserNameMaxLength) return (false, $"Username to long, must not exceed {UserNameMaxLength} characters."); + + var invalidChars = new List(); + + foreach(var c in userName) + { + if (char.IsPunctuation(c) || char.IsSymbol(c) || char.IsControl(c) || char.IsSeparator(c) || char.IsWhiteSpace(c)) invalidChars.Add(c);//return (false, $"The character '{c}' is not allowed."); + } + + if (invalidChars.Count > 0) return (false, $"The characters '{string.Join(",", invalidChars)}' are not allowed in a user name."); + + return (true, string.Empty); + } + public static (string PasswordHash, string SaltHash) GetUserPasswordHash(string userName) { var userId = GetUserId(userName);