|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using Aspire.Hosting.ApplicationModel;
namespace Aspire.Hosting.Utils;
internal static class PasswordGenerator
{
// Some chars are excluded:
// - prevent potential confusions, e.g., 0,o,O and i,I,l
// - exclude special chars which could interfere with command line arguments, URL (rfc3986 gen-delims), or connection strings, e.g. =,$,...
internal const string LowerCaseChars = "abcdefghjkmnpqrstuvwxyz"; // exclude i,l,o
internal const string UpperCaseChars = "ABCDEFGHJKMNPQRSTUVWXYZ"; // exclude I,L,O
internal const string NumericChars = "0123456789";
internal const string SpecialChars = "-_.{}~()*+!"; // exclude &<>=;,`'^%$#@/:[]
/// <summary>
/// Creates a cryptographically random string.
/// </summary>
/// <remarks>
/// <seealso cref="GenerateParameterDefault"/>.
/// </remarks>
public static string Generate(int minLength,
bool lower, bool upper, bool numeric, bool special,
int minLower, int minUpper, int minNumeric, int minSpecial)
{
ArgumentOutOfRangeException.ThrowIfNegative(minLength);
ArgumentOutOfRangeException.ThrowIfNegative(minLower);
ArgumentOutOfRangeException.ThrowIfNegative(minUpper);
ArgumentOutOfRangeException.ThrowIfNegative(minNumeric);
ArgumentOutOfRangeException.ThrowIfNegative(minSpecial);
CheckMinZeroWhenDisabled(lower, minLower);
CheckMinZeroWhenDisabled(upper, minUpper);
CheckMinZeroWhenDisabled(numeric, minNumeric);
CheckMinZeroWhenDisabled(special, minSpecial);
var requiredMinLength = checked(minLower + minUpper + minNumeric + minSpecial);
var length = Math.Max(minLength, requiredMinLength);
Span<char> chars = length <= 128 ? stackalloc char[length] : new char[length];
// fill the required characters first
var currentChars = chars;
GenerateRequiredValues(ref currentChars, minLower, LowerCaseChars);
GenerateRequiredValues(ref currentChars, minUpper, UpperCaseChars);
GenerateRequiredValues(ref currentChars, minNumeric, NumericChars);
GenerateRequiredValues(ref currentChars, minSpecial, SpecialChars);
// fill the rest of the password with random characters from all the available choices
var choices = GetChoices(lower, upper, numeric, special);
RandomNumberGenerator.GetItems(choices, currentChars);
RandomNumberGenerator.Shuffle(chars);
var result = new string(chars);
// clear the buffer so the password isn't in memory in multiple places
chars.Clear();
return result;
}
private static void CheckMinZeroWhenDisabled(
bool enabled,
int minValue,
[CallerArgumentExpression(nameof(enabled))] string? enabledParamName = null,
[CallerArgumentExpression(nameof(minValue))] string? minValueParamName = null)
{
if (!enabled && minValue > 0)
{
ThrowArgumentException();
}
void ThrowArgumentException() => throw new ArgumentException($"'{minValueParamName}' must be 0 if '{enabledParamName}' is disabled.");
}
private static void GenerateRequiredValues(ref Span<char> destination, int minValues, string choices)
{
Debug.Assert(destination.Length >= minValues);
if (minValues > 0)
{
RandomNumberGenerator.GetItems(choices, destination.Slice(0, minValues));
destination = destination.Slice(minValues);
}
}
private static string GetChoices(bool lower, bool upper, bool numeric, bool special) =>
(lower, upper, numeric, special) switch
{
(true, true, true, true) => LowerCaseChars + UpperCaseChars + NumericChars + SpecialChars,
(true, true, true, false) => LowerCaseChars + UpperCaseChars + NumericChars,
(true, true, false, true) => LowerCaseChars + UpperCaseChars + SpecialChars,
(true, true, false, false) => LowerCaseChars + UpperCaseChars,
(true, false, true, true) => LowerCaseChars + NumericChars + SpecialChars,
(true, false, true, false) => LowerCaseChars + NumericChars,
(true, false, false, true) => LowerCaseChars + SpecialChars,
(true, false, false, false) => LowerCaseChars,
(false, true, true, true) => UpperCaseChars + NumericChars + SpecialChars,
(false, true, true, false) => UpperCaseChars + NumericChars,
(false, true, false, true) => UpperCaseChars + SpecialChars,
(false, true, false, false) => UpperCaseChars,
(false, false, true, true) => NumericChars + SpecialChars,
(false, false, true, false) => NumericChars,
(false, false, false, true) => SpecialChars,
(false, false, false, false) => throw new ArgumentException("At least one character type must be enabled.")
};
}
|