Skip to content

Commit

Permalink
Ported Password Generator to DevToys 2.0 (#1001)
Browse files Browse the repository at this point in the history
* Ported Password Generator to DevToys 2.0

* Added unit tests
  • Loading branch information
veler authored Dec 16, 2023
1 parent 30b86ff commit ac5a289
Show file tree
Hide file tree
Showing 8 changed files with 1,296 additions and 0 deletions.
9 changes: 9 additions & 0 deletions src/app/dev/DevToys.Tools/DevToys.Tools.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@
<AutoGen>True</AutoGen>
<DependentUpon>HashAndChecksumGenerator.resx</DependentUpon>
</Compile>
<Compile Update="Tools\Generators\Password\PasswordGenerator.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>PasswordGenerator.resx</DependentUpon>
</Compile>
<Compile Update="Tools\Generators\UUID\UUIDGenerator.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
Expand Down Expand Up @@ -180,6 +185,10 @@
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>HashAndChecksumGenerator.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Tools\Generators\Password\PasswordGenerator.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>PasswordGenerator.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Tools\Generators\UUID\UUIDGenerator.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>UUIDGenerator.Designer.cs</LastGenOutput>
Expand Down
131 changes: 131 additions & 0 deletions src/app/dev/DevToys.Tools/Helpers/Core/CryptoRandom.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
using System.Security.Cryptography;

namespace DevToys.Tools.Helpers.Core;

/// <summary>
/// A class that mimics the standard Random class in the .NET Framework - but uses a random number generator internally.
/// Taken from IdentityModel (ref.: https://github.com/IdentityModel/IdentityModel/ )
/// Taken from PasswordGenerator (ref.: https://github.com/Darkseal/PasswordGenerator/blob/master/CryptoRandom.cs )
/// </summary>
internal sealed class CryptoRandom : Random
{
private static readonly RandomNumberGenerator Rng = RandomNumberGenerator.Create();
private readonly byte[] _uint32Buffer = new byte[4];

/// <summary>
/// Output format for unique IDs
/// </summary>
private enum OutputFormat
{
/// <summary>
/// URL-safe Base64
/// </summary>
Base64Url,
/// <summary>
/// Base64
/// </summary>
Base64,
/// <summary>
/// Hex
/// </summary>
Hex
}

/// <summary>
/// Initializes a new instance of the <see cref="CryptoRandom"/> class.
/// </summary>
internal CryptoRandom()
{
}

/// <summary>
/// Returns a nonnegative random number.
/// </summary>
/// <returns>
/// A 32-bit signed integer greater than or equal to zero and less than <see cref="F:System.Int32.MaxValue"/>.
/// </returns>
public override int Next()
{
Rng.GetBytes(_uint32Buffer);
return BitConverter.ToInt32(_uint32Buffer, 0) & 0x7FFFFFFF;
}

/// <summary>
/// Returns a nonnegative random number less than the specified maximum.
/// </summary>
/// <param name="maxValue">The exclusive upper bound of the random number to be generated. <paramref name="maxValue"/> must be greater than or equal to zero.</param>
/// <returns>
/// A 32-bit signed integer greater than or equal to zero, and less than <paramref name="maxValue"/>; that is, the range of return values ordinarily includes zero but not <paramref name="maxValue"/>. However, if <paramref name="maxValue"/> equals zero, <paramref name="maxValue"/> is returned.
/// </returns>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// <paramref name="maxValue"/> is less than zero.
/// </exception>
public override int Next(int maxValue)
{
if (maxValue < 0)
throw new ArgumentOutOfRangeException(nameof(maxValue));

return Next(0, maxValue);
}

/// <summary>
/// Returns a random number within a specified range.
/// </summary>
/// <param name="minValue">The inclusive lower bound of the random number returned.</param>
/// <param name="maxValue">The exclusive upper bound of the random number returned. <paramref name="maxValue"/> must be greater than or equal to <paramref name="minValue"/>.</param>
/// <returns>
/// A 32-bit signed integer greater than or equal to <paramref name="minValue"/> and less than <paramref name="maxValue"/>; that is, the range of return values includes <paramref name="minValue"/> but not <paramref name="maxValue"/>. If <paramref name="minValue"/> equals <paramref name="maxValue"/>, <paramref name="minValue"/> is returned.
/// </returns>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// <paramref name="minValue"/> is greater than <paramref name="maxValue"/>.
/// </exception>
public override int Next(int minValue, int maxValue)
{
if (minValue > maxValue)
throw new ArgumentOutOfRangeException(nameof(minValue));

if (minValue == maxValue)
return minValue;

long diff = maxValue - minValue;

while (true)
{
Rng.GetBytes(_uint32Buffer);
uint rand = BitConverter.ToUInt32(_uint32Buffer, 0);

long max = 1 + (long)uint.MaxValue;
long remainder = max % diff;
if (rand < max - remainder)
return (int)(minValue + rand % diff);
}
}

/// <summary>
/// Returns a random number between 0.0 and 1.0.
/// </summary>
/// <returns>
/// A double-precision floating point number greater than or equal to 0.0, and less than 1.0.
/// </returns>
public override double NextDouble()
{
Rng.GetBytes(_uint32Buffer);
uint rand = BitConverter.ToUInt32(_uint32Buffer, 0);
return rand / (1.0 + uint.MaxValue);
}

/// <summary>
/// Fills the elements of a specified array of bytes with random numbers.
/// </summary>
/// <param name="buffer">An array of bytes to contain random numbers.</param>
/// <exception cref="T:System.ArgumentNullException">
/// <paramref name="buffer"/> is null.
/// </exception>
public override void NextBytes(byte[] buffer)
{
if (buffer == null)
throw new ArgumentNullException(nameof(buffer));

Rng.GetBytes(buffer);
}
}
107 changes: 107 additions & 0 deletions src/app/dev/DevToys.Tools/Helpers/PasswordGeneratorHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System.Text;
using DevToys.Tools.Helpers.Core;

namespace DevToys.Tools.Helpers;

internal static class PasswordGeneratorHelper
{
/// <summary>
/// All non-alphanumeric characters.
/// </summary>
internal const string NonAlphanumeric = "!@#$%^&*";

/// <summary>
/// All lower case ASCII characters.
/// </summary>
internal const string LowercaseLetters = "abcdefghijkmnopqrstuvwxyz";

/// <summary>
/// All upper case ASCII characters.
/// </summary>
internal const string UppercaseLetters = "ABCDEFGHJKLMNOPQRSTUVWXYZ";

/// <summary>
/// All digits.
/// </summary>
internal const string Digits = "0123456789";

internal static string GeneratePassword(
int length,
bool hasUppercase,
bool hasLowercase,
bool hasNumbers,
bool hasSpecialCharacters,
char[]? excludedCharacters)
{
if (length <= 0)
{
return string.Empty;
}

// Combine all character sets together.
string[] randomChars = new[] {
string.Empty,
string.Empty,
string.Empty,
string.Empty
};

var rand = new CryptoRandom();
var newPasswordCharacters = new List<char>();

if (hasUppercase)
{
randomChars[0] = RemoveExcludedCharacters(UppercaseLetters, excludedCharacters);
}

if (hasLowercase)
{
randomChars[1] = RemoveExcludedCharacters(LowercaseLetters, excludedCharacters);
}

if (hasNumbers)
{
randomChars[2] = RemoveExcludedCharacters(Digits, excludedCharacters);
}

if (hasSpecialCharacters)
{
randomChars[3] = RemoveExcludedCharacters(NonAlphanumeric, excludedCharacters);
}

randomChars = randomChars.Where(r => r.Length > 0).ToArray();

// Only continue if the user hasn't excluded everything.
if (randomChars.Length != 0)
{
for (int j = 0; j < length; j++)
{
string rcs = randomChars[rand.Next(0, randomChars.Length)];
newPasswordCharacters.Insert(rand.Next(0, newPasswordCharacters.Count), rcs[rand.Next(0, rcs.Length)]);
}
}

return new string(newPasswordCharacters.ToArray());
}

private static string RemoveExcludedCharacters(string input, char[]? excludedCharacters)
{
if (excludedCharacters == null || excludedCharacters.Length == 0)
{
return input;
}

var excludedSet = new HashSet<char>(excludedCharacters); // HashSet provides a faster lookup than Array.Contains().
var stringBuilder = new StringBuilder();

foreach (char c in input)
{
if (!excludedSet.Contains(c))
{
stringBuilder.Append(c);
}
}

return stringBuilder.ToString();
}
}
Loading

0 comments on commit ac5a289

Please sign in to comment.