Password.cs
using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text.RegularExpressions; namespace PasswordGenerator { /// <summary> /// Generates random passwords and validates that they meet the rules passed in /// </summary> public class Password : IPassword { private const int DefaultPasswordLength = 16; private const int DefaultMaxPasswordAttempts = 10000; private const bool DefaultIncludeLowercase = true; private const bool DefaultIncludeUppercase = true; private const bool DefaultIncludeNumeric = true; private const bool DefaultIncludeSpecial = true; private static RandomNumberGenerator _rng; public Password() { Settings = new PasswordSettings(DefaultIncludeLowercase, DefaultIncludeUppercase, DefaultIncludeNumeric, DefaultIncludeSpecial, DefaultPasswordLength, DefaultMaxPasswordAttempts, true); _rng = RandomNumberGenerator.Create(); } public Password(IPasswordSettings settings) { Settings = settings; _rng = RandomNumberGenerator.Create(); } public Password(int passwordLength) { Settings = new PasswordSettings(DefaultIncludeLowercase, DefaultIncludeUppercase, DefaultIncludeNumeric, DefaultIncludeSpecial, passwordLength, DefaultMaxPasswordAttempts, true); _rng = RandomNumberGenerator.Create(); } public Password(bool includeLowercase, bool includeUppercase, bool includeNumeric, bool includeSpecial) { Settings = new PasswordSettings(includeLowercase, includeUppercase, includeNumeric, includeSpecial, DefaultPasswordLength, DefaultMaxPasswordAttempts, false); _rng = RandomNumberGenerator.Create(); } public Password(bool includeLowercase, bool includeUppercase, bool includeNumeric, bool includeSpecial, int passwordLength) { Settings = new PasswordSettings(includeLowercase, includeUppercase, includeNumeric, includeSpecial, passwordLength, DefaultMaxPasswordAttempts, false); _rng = RandomNumberGenerator.Create(); } public Password(bool includeLowercase, bool includeUppercase, bool includeNumeric, bool includeSpecial, int passwordLength, int maximumAttempts) { Settings = new PasswordSettings(includeLowercase, includeUppercase, includeNumeric, includeSpecial, passwordLength, maximumAttempts, false); _rng = RandomNumberGenerator.Create(); } public IPasswordSettings Settings { get; set; } public IPassword IncludeLowercase() { Settings = Settings.AddLowercase(); return this; } public IPassword IncludeUppercase() { Settings = Settings.AddUppercase(); return this; } public IPassword IncludeNumeric() { Settings = Settings.AddNumeric(); return this; } public IPassword IncludeSpecial() { Settings = Settings.AddSpecial(); return this; } public IPassword IncludeSpecial(string specialCharactersToInclude) { Settings = Settings.AddSpecial(specialCharactersToInclude); return this; } public IPassword LengthRequired(int passwordLength) { Settings.PasswordLength = passwordLength; return this; } /// <summary> /// Gets the next random password which meets the requirements /// </summary> /// <returns>A password as a string</returns> public string Next() { string password; if (!LengthIsValid(Settings.PasswordLength, Settings.MinimumLength, Settings.MaximumLength)) { password = $"Password length invalid. Must be between {Settings.MinimumLength} and {Settings.MaximumLength} characters long"; } else { var passwordAttempts = 0; do { password = GenerateRandomPassword(Settings); passwordAttempts++; } while (passwordAttempts < Settings.MaximumAttempts && !PasswordIsValid(Settings, password)); password = PasswordIsValid(Settings, password) ? password : "Try again"; } return password; } public IEnumerable<string> NextGroup(int numberOfPasswordsToGenerate) { var passwords = new List<string>(); for (var i = 0; i < numberOfPasswordsToGenerate; i++) { var pwd = this.Next(); passwords.Add(pwd); } return passwords; } /// <summary> /// Generates a random password based on the rules passed in the settings parameter /// This does not do any validation /// </summary> /// <param name="settings">Password generator settings object</param> /// <returns>a random password</returns> private static string GenerateRandomPassword(IPasswordSettings settings) { const int maximumIdenticalConsecutiveChars = 2; var password = new char[settings.PasswordLength]; var characters = settings.CharacterSet.ToCharArray(); var shuffledChars = Shuffle(characters.Select(x => x)).ToArray(); var shuffledCharacterSet = string.Join(null, shuffledChars); var characterSetLength = shuffledCharacterSet.Length; for (var characterPosition = 0; characterPosition < settings.PasswordLength; characterPosition++) { password[characterPosition] = shuffledCharacterSet[GetRandomNumberInRange(0,characterSetLength - 1)]; var moreThanTwoIdenticalInARow = characterPosition > maximumIdenticalConsecutiveChars && password[characterPosition] == password[characterPosition - 1] && password[characterPosition - 1] == password[characterPosition - 2]; if (moreThanTwoIdenticalInARow) characterPosition--; } return string.Join(null, password); } private static int GetRandomNumberInRange(int min, int max) { if (min > max) throw new ArgumentOutOfRangeException(); var data = new byte[sizeof(int)]; _rng.GetBytes(data); var randomNumber = BitConverter.ToInt32(data, 0); return (int)Math.Floor((double)(min + Math.Abs(randomNumber % (max - min)))); } private static int GetRngCryptoSeed(RNGCryptoServiceProvider rng) { var rngByteArray = new byte[4]; rng.GetBytes(rngByteArray); return BitConverter.ToInt32(rngByteArray, 0); } /// <summary> /// When you give it a password and some _settings, it validates the password against the _settings. /// </summary> /// <param name="settings">Password settings</param> /// <param name="password">Password to test</param> /// <returns>True or False to say if the password is valid or not</returns> private static bool PasswordIsValid(IPasswordSettings settings, string password) { const string regexLowercase = @"[a-z]"; const string regexUppercase = @"[A-Z]"; const string regexNumeric = @"[\d]"; var lowerCaseIsValid = !settings.IncludeLowercase || settings.IncludeLowercase && Regex.IsMatch(password, regexLowercase); var upperCaseIsValid = !settings.IncludeUppercase || settings.IncludeUppercase && Regex.IsMatch(password, regexUppercase); var numericIsValid = !settings.IncludeNumeric || settings.IncludeNumeric && Regex.IsMatch(password, regexNumeric); var specialIsValid = !settings.IncludeSpecial; if (settings.IncludeSpecial && !string.IsNullOrWhiteSpace(settings.SpecialCharacters)) { var listA = settings.SpecialCharacters.ToCharArray(); var listB = password.ToCharArray(); specialIsValid = listA.Any(x => listB.Contains(x)); } return lowerCaseIsValid && upperCaseIsValid && numericIsValid && specialIsValid && LengthIsValid(password.Length, settings.MinimumLength, settings.MaximumLength); } /// <summary> /// Checks that the password is within the valid length range /// </summary> /// <param name="passwordLength">The length of the password</param> /// <param name="minLength">The minimum allowed length</param> /// <param name="maxLength">The maximum allowed length</param> /// <returns>A bool to say if it is valid or not</returns> private static bool LengthIsValid(int passwordLength, int minLength, int maxLength) { return passwordLength >= minLength && passwordLength <= maxLength; } private static IEnumerable<T> Shuffle<T>(IEnumerable<T> items) { return from item in items orderby Guid.NewGuid() select item; } } } |