using System.Security.Cryptography; using Umbraco.Cms.Core.Configuration; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Security; /// /// Generates a password /// /// /// This uses logic copied from the old MembershipProvider.GeneratePassword logic /// public class PasswordGenerator { private readonly IPasswordConfiguration _passwordConfiguration; public PasswordGenerator(IPasswordConfiguration passwordConfiguration) => _passwordConfiguration = passwordConfiguration; public string GeneratePassword() { var password = PasswordStore.GeneratePassword( _passwordConfiguration.RequiredLength, _passwordConfiguration.GetMinNonAlphaNumericChars()); var passwordChars = password.ToCharArray(); if (_passwordConfiguration.RequireDigit && passwordChars.ContainsAny(Enumerable.Range(48, 58).Select(x => (char)x))) { password += Convert.ToChar(RandomNumberGenerator.GetInt32(48, 58)); // 0-9 } if (_passwordConfiguration.RequireLowercase && passwordChars.ContainsAny(Enumerable.Range(97, 123).Select(x => (char)x))) { password += Convert.ToChar(RandomNumberGenerator.GetInt32(97, 123)); // a-z } if (_passwordConfiguration.RequireUppercase && passwordChars.ContainsAny(Enumerable.Range(65, 91).Select(x => (char)x))) { password += Convert.ToChar(RandomNumberGenerator.GetInt32(65, 91)); // A-Z } if (_passwordConfiguration.RequireNonLetterOrDigit && passwordChars.ContainsAny(Enumerable.Range(33, 48).Select(x => (char)x))) { password += Convert.ToChar(RandomNumberGenerator.GetInt32(33, 48)); // symbols !"#$%&'()*+,-./ } return password; } /// /// Internal class copied from ASP.NET Framework MembershipProvider /// /// /// See https://stackoverflow.com/a/39855417/694494 + /// https://github.com/Microsoft/referencesource/blob/master/System.Web/Security/Membership.cs /// private static class PasswordStore { private static readonly char[] Punctuations = "!@#$%^&*()_-+=[{]};:>|./?".ToCharArray(); private static readonly char[] StartingChars = { '<', '&' }; /// Generates a random password of the specified length. /// A random password of the specified length. /// /// The number of characters in the generated password. The length must be between 1 and 128 /// characters. /// /// /// The minimum number of non-alphanumeric characters (such as @, #, !, %, /// &, and so on) in the generated password. /// /// /// is less than 1 or greater than 128 -or- /// is less than 0 or greater than . /// public static string GeneratePassword(int length, int numberOfNonAlphanumericCharacters) { if (length < 1 || length > 128) { throw new ArgumentException("password length incorrect", nameof(length)); } if (numberOfNonAlphanumericCharacters > length || numberOfNonAlphanumericCharacters < 0) { throw new ArgumentException( "min required non alphanumeric characters incorrect", nameof(numberOfNonAlphanumericCharacters)); } string s; do { var data = new byte[length]; var chArray = new char[length]; var num1 = 0; using (var rng = new RNGCryptoServiceProvider()) { rng.GetBytes(data); } for (var index = 0; index < length; ++index) { var num2 = data[index] % 87; if (num2 < 10) { chArray[index] = (char)(48 + num2); } else if (num2 < 36) { chArray[index] = (char)(65 + num2 - 10); } else if (num2 < 62) { chArray[index] = (char)(97 + num2 - 36); } else { chArray[index] = Punctuations[num2 - 62]; ++num1; } } if (num1 < numberOfNonAlphanumericCharacters) { for (var index1 = 0; index1 < numberOfNonAlphanumericCharacters - num1; ++index1) { int index2; do { index2 = RandomNumberGenerator.GetInt32(0, length); } while (!char.IsLetterOrDigit(chArray[index2])); chArray[index2] = Punctuations[RandomNumberGenerator.GetInt32(0, Punctuations.Length)]; } } s = new string(chArray); } while (IsDangerousString(s, out int matchIndex)); return s; } private static bool IsDangerousString(string s, out int matchIndex) { // bool inComment = false; matchIndex = 0; for (var i = 0; ;) { // Look for the start of one of our patterns var n = s.IndexOfAny(StartingChars, i); // If not found, the string is safe if (n < 0) { return false; } // If it's the last char, it's safe if (n == s.Length - 1) { return false; } matchIndex = n; switch (s[n]) { case '<': // If the < is followed by a letter or '!', it's unsafe (looks like a tag or HTML comment) if (IsAtoZ(s[n + 1]) || s[n + 1] == '!' || s[n + 1] == '/' || s[n + 1] == '?') { return true; } break; case '&': // If the & is followed by a #, it's unsafe (e.g. S) if (s[n + 1] == '#') { return true; } break; } // Continue searching i = n + 1; } } private static bool IsAtoZ(char c) { if (c >= 97 && c <= 122) { return true; } if (c >= 65) { return c <= 90; } return false; } } }