2019-11-25 21:20:00 +11:00
using System ;
using System.Linq ;
using System.Security.Cryptography ;
2021-02-18 11:06:02 +01:00
using Umbraco.Cms.Core.Configuration ;
using Umbraco.Extensions ;
2019-11-25 21:20:00 +11:00
2021-02-18 11:06:02 +01:00
namespace Umbraco.Cms.Core.Security
2019-11-25 21:20:00 +11:00
{
/// <summary>
/// Generates a password
/// </summary>
/// <remarks>
/// This uses logic copied from the old MembershipProvider.GeneratePassword logic
/// </remarks>
2019-12-03 15:28:55 +11:00
public class PasswordGenerator
2019-11-25 21:20:00 +11:00
{
2019-12-03 15:28:55 +11:00
private readonly IPasswordConfiguration _passwordConfiguration ;
public PasswordGenerator ( IPasswordConfiguration passwordConfiguration )
{
_passwordConfiguration = passwordConfiguration ;
}
public string GeneratePassword ( )
2019-11-25 21:20:00 +11:00
{
var password = PasswordStore . GeneratePassword (
2019-12-03 15:28:55 +11:00
_passwordConfiguration . RequiredLength ,
2021-04-20 19:34:18 +02:00
_passwordConfiguration . GetMinNonAlphaNumericChars ( ) ) ;
2019-11-25 21:20:00 +11:00
var random = new Random ( ) ;
var passwordChars = password . ToCharArray ( ) ;
2019-12-03 15:28:55 +11:00
if ( _passwordConfiguration . RequireDigit & & passwordChars . ContainsAny ( Enumerable . Range ( 48 , 58 ) . Select ( x = > ( char ) x ) ) )
2019-11-25 21:20:00 +11:00
password + = Convert . ToChar ( random . Next ( 48 , 58 ) ) ; // 0-9
2019-12-03 15:28:55 +11:00
if ( _passwordConfiguration . RequireLowercase & & passwordChars . ContainsAny ( Enumerable . Range ( 97 , 123 ) . Select ( x = > ( char ) x ) ) )
2019-11-25 21:20:00 +11:00
password + = Convert . ToChar ( random . Next ( 97 , 123 ) ) ; // a-z
2019-12-03 15:28:55 +11:00
if ( _passwordConfiguration . RequireUppercase & & passwordChars . ContainsAny ( Enumerable . Range ( 65 , 91 ) . Select ( x = > ( char ) x ) ) )
2019-11-25 21:20:00 +11:00
password + = Convert . ToChar ( random . Next ( 65 , 91 ) ) ; // A-Z
2019-12-03 15:28:55 +11:00
if ( _passwordConfiguration . RequireNonLetterOrDigit & & passwordChars . ContainsAny ( Enumerable . Range ( 33 , 48 ) . Select ( x = > ( char ) x ) ) )
2019-11-25 21:20:00 +11:00
password + = Convert . ToChar ( random . Next ( 33 , 48 ) ) ; // symbols !"#$%&'()*+,-./
return password ;
}
/// <summary>
/// Internal class copied from ASP.NET Framework MembershipProvider
/// </summary>
/// <remarks>
/// See https://stackoverflow.com/a/39855417/694494 + https://github.com/Microsoft/referencesource/blob/master/System.Web/Security/Membership.cs
/// </remarks>
private static class PasswordStore
{
private static readonly char [ ] Punctuations = "!@#$%^&*()_-+=[{]};:>|./?" . ToCharArray ( ) ;
private static readonly char [ ] StartingChars = new char [ ] { '<' , '&' } ;
/// <summary>Generates a random password of the specified length.</summary>
/// <returns>A random password of the specified length.</returns>
/// <param name="length">The number of characters in the generated password. The length must be between 1 and 128 characters. </param>
/// <param name="numberOfNonAlphanumericCharacters">The minimum number of non-alphanumeric characters (such as @, #, !, %, &, and so on) in the generated password.</param>
/// <exception cref="T:System.ArgumentException">
/// <paramref name="length" /> is less than 1 or greater than 128 -or-<paramref name="numberOfNonAlphanumericCharacters" /> is less than 0 or greater than <paramref name="length" />. </exception>
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 ;
int matchIndex ;
do
{
var data = new byte [ length ] ;
var chArray = new char [ length ] ;
var num1 = 0 ;
new RNGCryptoServiceProvider ( ) . GetBytes ( data ) ;
for ( var index = 0 ; index < length ; + + index )
{
var num2 = ( int ) 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 )
{
var random = new Random ( ) ;
for ( var index1 = 0 ; index1 < numberOfNonAlphanumericCharacters - num1 ; + + index1 )
{
int index2 ;
do
{
index2 = random . Next ( 0 , length ) ;
}
while ( ! char . IsLetterOrDigit ( chArray [ index2 ] ) ) ;
chArray [ index2 ] = Punctuations [ random . Next ( 0 , Punctuations . Length ) ] ;
}
}
s = new string ( chArray ) ;
}
while ( IsDangerousString ( s , out matchIndex ) ) ;
return s ;
}
private static bool IsDangerousString ( string s , out int matchIndex )
{
//bool inComment = false;
matchIndex = 0 ;
for ( var i = 0 ; ; )
{
2021-02-18 11:06:02 +01:00
// Look for the start of one of our patterns
2019-11-25 21:20:00 +11:00
var n = s . IndexOfAny ( StartingChars , i ) ;
// If not found, the string is safe
if ( n < 0 ) return false ;
2021-02-18 11:06:02 +01:00
// If it's the last char, it's safe
2019-11-25 21:20:00 +11:00
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 '&' :
2021-02-18 11:06:02 +01:00
// If the & is followed by a #, it's unsafe (e.g. S)
2019-11-25 21:20:00 +11:00
if ( s [ n + 1 ] = = '#' ) return true ;
break ;
}
// Continue searching
i = n + 1 ;
}
}
private static bool IsAtoZ ( char c )
{
if ( ( int ) c > = 97 & & ( int ) c < = 122 )
return true ;
if ( ( int ) c > = 65 )
return ( int ) c < = 90 ;
return false ;
}
}
}
}