From 3c6700d3936ee48ef577e28b58e1332b5abafe3f Mon Sep 17 00:00:00 2001 From: Etienne Baudoux Date: Wed, 13 Dec 2023 21:30:06 -0800 Subject: [PATCH 1/2] Ported Password Generator to DevToys 2.0 --- .../dev/DevToys.Tools/DevToys.Tools.csproj | 18 + .../Helpers/Core/CryptoRandom.cs | 131 +++++++ .../Helpers/PasswordGeneratorHelper.cs | 115 ++++++ .../Password/PasswordGenerator.Designer.cs | 369 ++++++++++++++++++ .../Password/PasswordGenerator.resx | 222 +++++++++++ .../PasswordGeneratorCommandLineTool.cs | 65 +++ .../Password/PasswordGeneratorGuidTool.cs | 329 ++++++++++++++++ 7 files changed, 1249 insertions(+) create mode 100644 src/app/dev/DevToys.Tools/Helpers/Core/CryptoRandom.cs create mode 100644 src/app/dev/DevToys.Tools/Helpers/PasswordGeneratorHelper.cs create mode 100644 src/app/dev/DevToys.Tools/Tools/Generators/Password/PasswordGenerator.Designer.cs create mode 100644 src/app/dev/DevToys.Tools/Tools/Generators/Password/PasswordGenerator.resx create mode 100644 src/app/dev/DevToys.Tools/Tools/Generators/Password/PasswordGeneratorCommandLineTool.cs create mode 100644 src/app/dev/DevToys.Tools/Tools/Generators/Password/PasswordGeneratorGuidTool.cs diff --git a/src/app/dev/DevToys.Tools/DevToys.Tools.csproj b/src/app/dev/DevToys.Tools/DevToys.Tools.csproj index cb1255575b..56439c36c7 100644 --- a/src/app/dev/DevToys.Tools/DevToys.Tools.csproj +++ b/src/app/dev/DevToys.Tools/DevToys.Tools.csproj @@ -84,6 +84,16 @@ True HashAndChecksumGenerator.resx + + True + True + PasswordGenerator.resx + + + True + True + UUIDGenerator.resx + True True @@ -175,6 +185,14 @@ ResXFileCodeGenerator HashAndChecksumGenerator.Designer.cs + + ResXFileCodeGenerator + PasswordGenerator.Designer.cs + + + ResXFileCodeGenerator + UUIDGenerator.Designer.cs + ResXFileCodeGenerator ColorBlindnessSimulator.Designer.cs diff --git a/src/app/dev/DevToys.Tools/Helpers/Core/CryptoRandom.cs b/src/app/dev/DevToys.Tools/Helpers/Core/CryptoRandom.cs new file mode 100644 index 0000000000..4e2fef8b8a --- /dev/null +++ b/src/app/dev/DevToys.Tools/Helpers/Core/CryptoRandom.cs @@ -0,0 +1,131 @@ +using System.Security.Cryptography; + +namespace DevToys.Tools.Helpers.Core; + +/// +/// 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 ) +/// +internal sealed class CryptoRandom : Random +{ + private static readonly RandomNumberGenerator Rng = RandomNumberGenerator.Create(); + private readonly byte[] _uint32Buffer = new byte[4]; + + /// + /// Output format for unique IDs + /// + private enum OutputFormat + { + /// + /// URL-safe Base64 + /// + Base64Url, + /// + /// Base64 + /// + Base64, + /// + /// Hex + /// + Hex + } + + /// + /// Initializes a new instance of the class. + /// + internal CryptoRandom() + { + } + + /// + /// Returns a nonnegative random number. + /// + /// + /// A 32-bit signed integer greater than or equal to zero and less than . + /// + public override int Next() + { + Rng.GetBytes(_uint32Buffer); + return BitConverter.ToInt32(_uint32Buffer, 0) & 0x7FFFFFFF; + } + + /// + /// Returns a nonnegative random number less than the specified maximum. + /// + /// The exclusive upper bound of the random number to be generated. must be greater than or equal to zero. + /// + /// A 32-bit signed integer greater than or equal to zero, and less than ; that is, the range of return values ordinarily includes zero but not . However, if equals zero, is returned. + /// + /// + /// is less than zero. + /// + public override int Next(int maxValue) + { + if (maxValue < 0) + throw new ArgumentOutOfRangeException(nameof(maxValue)); + + return Next(0, maxValue); + } + + /// + /// Returns a random number within a specified range. + /// + /// The inclusive lower bound of the random number returned. + /// The exclusive upper bound of the random number returned. must be greater than or equal to . + /// + /// A 32-bit signed integer greater than or equal to and less than ; that is, the range of return values includes but not . If equals , is returned. + /// + /// + /// is greater than . + /// + 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); + } + } + + /// + /// Returns a random number between 0.0 and 1.0. + /// + /// + /// A double-precision floating point number greater than or equal to 0.0, and less than 1.0. + /// + public override double NextDouble() + { + Rng.GetBytes(_uint32Buffer); + uint rand = BitConverter.ToUInt32(_uint32Buffer, 0); + return rand / (1.0 + uint.MaxValue); + } + + /// + /// Fills the elements of a specified array of bytes with random numbers. + /// + /// An array of bytes to contain random numbers. + /// + /// is null. + /// + public override void NextBytes(byte[] buffer) + { + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + + Rng.GetBytes(buffer); + } +} diff --git a/src/app/dev/DevToys.Tools/Helpers/PasswordGeneratorHelper.cs b/src/app/dev/DevToys.Tools/Helpers/PasswordGeneratorHelper.cs new file mode 100644 index 0000000000..09b0f5bbb1 --- /dev/null +++ b/src/app/dev/DevToys.Tools/Helpers/PasswordGeneratorHelper.cs @@ -0,0 +1,115 @@ +using System.Text; +using DevToys.Tools.Helpers.Core; + +namespace DevToys.Tools.Helpers; + +internal static class PasswordGeneratorHelper +{ + /// + /// All non-alphanumeric characters. + /// + private const string NonAlphanumeric = "!@#$%^&*"; + + /// + /// All lower case ASCII characters. + /// + private const string LowercaseLetters = "abcdefghijkmnopqrstuvwxyz"; + + /// + /// All upper case ASCII characters. + /// + private const string UppercaseLetters = "ABCDEFGHJKLMNOPQRSTUVWXYZ"; + + /// + /// All digits. + /// + private const string Digits = "0123456789"; + + internal static string GeneratePassword( + int length, + bool hasUppercase, + bool hasLowercase, + bool hasNumbers, + bool hasSpecialCharacters, + char[] excludedCharacters) + { + // Combine all character sets together. + string[] randomChars = new[] { + string.Empty, + string.Empty, + string.Empty, + string.Empty + }; + + var rand = new CryptoRandom(); + var newPasswordCharacters = new List(); + + if (hasUppercase) + { + randomChars[0] = RemoveExcludedCharacters(UppercaseLetters, excludedCharacters); + + // If the whole set gets excluded don't include it. + if (randomChars[0].Length != 0) + { + newPasswordCharacters.Insert(rand.Next(0, newPasswordCharacters.Count), randomChars[0][rand.Next(0, randomChars[0].Length)]); + } + } + + if (hasLowercase) + { + randomChars[1] = RemoveExcludedCharacters(LowercaseLetters, excludedCharacters); + if (randomChars[1].Length != 0) + { + newPasswordCharacters.Insert(rand.Next(0, newPasswordCharacters.Count), randomChars[1][rand.Next(0, randomChars[1].Length)]); + } + } + + if (hasNumbers) + { + randomChars[2] = RemoveExcludedCharacters(Digits, excludedCharacters); + if (randomChars[2].Length != 0) + { + newPasswordCharacters.Insert(rand.Next(0, newPasswordCharacters.Count), randomChars[2][rand.Next(0, randomChars[2].Length)]); + } + } + + if (hasSpecialCharacters) + { + randomChars[3] = RemoveExcludedCharacters(NonAlphanumeric, excludedCharacters); + if (randomChars[3].Length != 0) + { + newPasswordCharacters.Insert(rand.Next(0, newPasswordCharacters.Count), randomChars[3][rand.Next(0, randomChars[3].Length)]); + } + } + + randomChars = randomChars.Where(r => r.Length > 0).ToArray(); + + // Only continue if the user hasn't excluded everything. + if (randomChars.Length != 0) + { + for (int j = newPasswordCharacters.Count; 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) + { + var excludedSet = new HashSet(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(); + } +} diff --git a/src/app/dev/DevToys.Tools/Tools/Generators/Password/PasswordGenerator.Designer.cs b/src/app/dev/DevToys.Tools/Tools/Generators/Password/PasswordGenerator.Designer.cs new file mode 100644 index 0000000000..515212b5dd --- /dev/null +++ b/src/app/dev/DevToys.Tools/Tools/Generators/Password/PasswordGenerator.Designer.cs @@ -0,0 +1,369 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace DevToys.Tools.Tools.Generators.Password { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class PasswordGenerator { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal PasswordGenerator() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DevToys.Tools.Tools.Generators.Password.PasswordGenerator", typeof(PasswordGenerator).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Password Generator tool. + /// + internal static string AccessibleName { + get { + return ResourceManager.GetString("AccessibleName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Configuration. + /// + internal static string ConfigurationTitle { + get { + return ResourceManager.GetString("ConfigurationTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generate random passwords. + /// + internal static string Description { + get { + return ResourceManager.GetString("Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Digit characters. + /// + internal static string Digits { + get { + return ResourceManager.GetString("Digits", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use digit characters (0123456789). + /// + internal static string DigitsDescription { + get { + return ResourceManager.GetString("DigitsDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Digits. + /// + internal static string DigitsDescriptionStateOn { + get { + return ResourceManager.GetString("DigitsDescriptionStateOn", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether the generated password can include digits characters (0123456789). + /// + internal static string DigitsOptionDescription { + get { + return ResourceManager.GetString("DigitsOptionDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ExcludedCharacters. + /// + internal static string ExcludeCharacters { + get { + return ResourceManager.GetString("ExcludeCharacters", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do not use the following characters to generate a password. + /// + internal static string ExcludeCharactersDescription { + get { + return ResourceManager.GetString("ExcludeCharactersDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Excluding '{0}'. + /// + internal static string ExcludedCharactersDescriptionState { + get { + return ResourceManager.GetString("ExcludedCharactersDescriptionState", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A set of characters that should not be used to generate the password. + /// + internal static string ExcludedCharactersOptionDescription { + get { + return ResourceManager.GetString("ExcludedCharactersOptionDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generate Password(s). + /// + internal static string GenerateButton { + get { + return ResourceManager.GetString("GenerateButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generate. + /// + internal static string GenerateTitle { + get { + return ResourceManager.GetString("GenerateTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Length. + /// + internal static string Length { + get { + return ResourceManager.GetString("Length", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The length of the password to generate. + /// + internal static string LengthOptionDescription { + get { + return ResourceManager.GetString("LengthOptionDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Password Generator. + /// + internal static string LongDisplayTitle { + get { + return ResourceManager.GetString("LongDisplayTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lowercase characters. + /// + internal static string Lowercase { + get { + return ResourceManager.GetString("Lowercase", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use lowercase characters (abcdefghijkmnopqrstuvwxyz). + /// + internal static string LowercaseDescription { + get { + return ResourceManager.GetString("LowercaseDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lowercase. + /// + internal static string LowercaseDescriptionStateOn { + get { + return ResourceManager.GetString("LowercaseDescriptionStateOn", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether the generated password can include lowercase characters (abcdefghijkmnopqrstuvwxyz). + /// + internal static string LowercaseOptionDescription { + get { + return ResourceManager.GetString("LowercaseOptionDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to x. + /// + internal static string MultiplySymbol { + get { + return ResourceManager.GetString("MultiplySymbol", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No password(s) can be generated because no options have been selected.. + /// + internal static string NoCharacterSetsWarning { + get { + return ResourceManager.GetString("NoCharacterSetsWarning", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Number of Passwords to generate. + /// + internal static string NumberOfPasswordsToGenerate_AutomationProperties_Name { + get { + return ResourceManager.GetString("NumberOfPasswordsToGenerate_AutomationProperties_Name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Passwords. + /// + internal static string Output { + get { + return ResourceManager.GetString("Output", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Passkey Secret Pwd. + /// + internal static string SearchKeywords { + get { + return ResourceManager.GetString("SearchKeywords", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Password. + /// + internal static string ShortDisplayTitle { + get { + return ResourceManager.GetString("ShortDisplayTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Special characters. + /// + internal static string SpecialCharacters { + get { + return ResourceManager.GetString("SpecialCharacters", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use special characters (!@#$%^&*). + /// + internal static string SpecialCharactersDescription { + get { + return ResourceManager.GetString("SpecialCharactersDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Special characters. + /// + internal static string SpecialCharactersDescriptionStateOn { + get { + return ResourceManager.GetString("SpecialCharactersDescriptionStateOn", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether the generated password can include special characters (!@#$%^&*). + /// + internal static string SpecialCharactersOptionDescription { + get { + return ResourceManager.GetString("SpecialCharactersOptionDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Uppercase characters. + /// + internal static string Uppercase { + get { + return ResourceManager.GetString("Uppercase", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use uppercase characters (ABCDEFGHJKLMNOPQRSTUVWXYZ). + /// + internal static string UppercaseDescription { + get { + return ResourceManager.GetString("UppercaseDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Uppercase. + /// + internal static string UppercaseDescriptionStateOn { + get { + return ResourceManager.GetString("UppercaseDescriptionStateOn", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether the generated password can include uppercase characters (ABCDEFGHJKLMNOPQRSTUVWXYZ). + /// + internal static string UppercaseOptionDescription { + get { + return ResourceManager.GetString("UppercaseOptionDescription", resourceCulture); + } + } + } +} diff --git a/src/app/dev/DevToys.Tools/Tools/Generators/Password/PasswordGenerator.resx b/src/app/dev/DevToys.Tools/Tools/Generators/Password/PasswordGenerator.resx new file mode 100644 index 0000000000..fe1b69877e --- /dev/null +++ b/src/app/dev/DevToys.Tools/Tools/Generators/Password/PasswordGenerator.resx @@ -0,0 +1,222 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Password Generator tool + + + Configuration + + + Generate random passwords + + + Digit characters + + + Use digit characters (0123456789) + + + Digits + + + Whether the generated password can include digits characters (0123456789) + + + ExcludedCharacters + + + Do not use the following characters to generate a password + + + Excluding '{0}' + + + A set of characters that should not be used to generate the password + + + Generate Password(s) + + + Generate + + + Length + + + The length of the password to generate + + + Password Generator + + + Lowercase characters + + + Use lowercase characters (abcdefghijkmnopqrstuvwxyz) + + + Lowercase + + + Whether the generated password can include lowercase characters (abcdefghijkmnopqrstuvwxyz) + + + x + + + No password(s) can be generated because no options have been selected. + + + Number of Passwords to generate + + + Passwords + + + Passkey Secret Pwd + + + Password + + + Special characters + + + Use special characters (!@#$%^&*) + + + Special characters + + + Whether the generated password can include special characters (!@#$%^&*) + + + Uppercase characters + + + Use uppercase characters (ABCDEFGHJKLMNOPQRSTUVWXYZ) + + + Uppercase + + + Whether the generated password can include uppercase characters (ABCDEFGHJKLMNOPQRSTUVWXYZ) + + \ No newline at end of file diff --git a/src/app/dev/DevToys.Tools/Tools/Generators/Password/PasswordGeneratorCommandLineTool.cs b/src/app/dev/DevToys.Tools/Tools/Generators/Password/PasswordGeneratorCommandLineTool.cs new file mode 100644 index 0000000000..52ff352bb7 --- /dev/null +++ b/src/app/dev/DevToys.Tools/Tools/Generators/Password/PasswordGeneratorCommandLineTool.cs @@ -0,0 +1,65 @@ +using DevToys.Tools.Helpers; +using Microsoft.Extensions.Logging; + +namespace DevToys.Tools.Tools.Generators.Password; + +[Export(typeof(ICommandLineTool))] +[Name("PasswordGenerator")] +[CommandName( + Name = "password", + Alias = "pwd", + ResourceManagerBaseName = "DevToys.Tools.Tools.Generators.Password.PasswordGenerator", + DescriptionResourceName = nameof(PasswordGenerator.Description))] +internal sealed class PasswordGeneratorCommandLineTool : ICommandLineTool +{ + [CommandLineOption( + Name = "length", + Alias = "l", + DescriptionResourceName = nameof(PasswordGenerator.LengthOptionDescription))] + internal int Length { get; set; } = 30; + + [CommandLineOption( + Name = "uppercase", + Alias = "u", + DescriptionResourceName = nameof(PasswordGenerator.UppercaseOptionDescription))] + internal bool Uppercase { get; set; } = true; + + [CommandLineOption( + Name = "lowercase", + Alias = "m", + DescriptionResourceName = nameof(PasswordGenerator.LowercaseOptionDescription))] + internal bool Lowercase { get; set; } = true; + + [CommandLineOption( + Name = "digits", + Alias = "d", + DescriptionResourceName = nameof(PasswordGenerator.DigitsOptionDescription))] + internal bool Digits { get; set; } = true; + + [CommandLineOption( + Name = "special", + Alias = "s", + DescriptionResourceName = nameof(PasswordGenerator.SpecialCharactersOptionDescription))] + internal bool SpecialCharacters { get; set; } = true; + + [CommandLineOption( + Name = "excluded", + Alias = "e", + DescriptionResourceName = nameof(PasswordGenerator.ExcludedCharactersOptionDescription))] + internal string ExcludedCharacters { get; set; } = string.Empty; + + public ValueTask InvokeAsync(ILogger logger, CancellationToken cancellationToken) + { + string password + = PasswordGeneratorHelper.GeneratePassword( + Length, + Uppercase, + Lowercase, + Digits, + SpecialCharacters, + ExcludedCharacters.ToArray()); + + Console.WriteLine(password); + return ValueTask.FromResult(0); + } +} diff --git a/src/app/dev/DevToys.Tools/Tools/Generators/Password/PasswordGeneratorGuidTool.cs b/src/app/dev/DevToys.Tools/Tools/Generators/Password/PasswordGeneratorGuidTool.cs new file mode 100644 index 0000000000..f01816b898 --- /dev/null +++ b/src/app/dev/DevToys.Tools/Tools/Generators/Password/PasswordGeneratorGuidTool.cs @@ -0,0 +1,329 @@ +using System.Text; +using DevToys.Tools.Helpers; + +namespace DevToys.Tools.Tools.Generators.Password; + +[Export(typeof(IGuiTool))] +[Name("PasswordGenerator")] +[ToolDisplayInformation( + IconFontName = "FluentSystemIcons", + IconGlyph = '\uE8C9', + GroupName = PredefinedCommonToolGroupNames.Generators, + ResourceManagerAssemblyIdentifier = nameof(DevToysToolsResourceManagerAssemblyIdentifier), + ResourceManagerBaseName = "DevToys.Tools.Tools.Generators.Password.PasswordGenerator", + ShortDisplayTitleResourceName = nameof(PasswordGenerator.ShortDisplayTitle), + LongDisplayTitleResourceName = nameof(PasswordGenerator.LongDisplayTitle), + DescriptionResourceName = nameof(PasswordGenerator.Description), + AccessibleNameResourceName = nameof(PasswordGenerator.AccessibleName))] +internal sealed class PasswordGeneratorGuidTool : IGuiTool +{ + /// + /// Whether the password should include uppercase characters. + /// + private static readonly SettingDefinition uppercase + = new( + name: $"{nameof(PasswordGeneratorGuidTool)}.{nameof(uppercase)}", + defaultValue: true); + + /// + /// Whether the password should include lowercase characters. + /// + private static readonly SettingDefinition lowercase + = new( + name: $"{nameof(PasswordGeneratorGuidTool)}.{nameof(lowercase)}", + defaultValue: true); + + /// + /// Whether the password should include numbers. + /// + private static readonly SettingDefinition numbers + = new( + name: $"{nameof(PasswordGeneratorGuidTool)}.{nameof(numbers)}", + defaultValue: true); + + /// + /// Whether the password should include special characters. + /// + private static readonly SettingDefinition specialCharacters + = new( + name: $"{nameof(PasswordGeneratorGuidTool)}.{nameof(specialCharacters)}", + defaultValue: true); + + /// + /// Excluded characters from password. + /// + private static readonly SettingDefinition excludedCharacters + = new( + name: $"{nameof(PasswordGeneratorGuidTool)}.{nameof(excludedCharacters)}", + defaultValue: string.Empty); + + /// + /// How long the password should be. + /// + private static readonly SettingDefinition length + = new( + name: $"{nameof(PasswordGeneratorGuidTool)}.{nameof(length)}", + defaultValue: 30); + + /// + /// How many passwords should be generated at once. + /// + private static readonly SettingDefinition passwordsToGenerate + = new( + name: $"{nameof(PasswordGeneratorGuidTool)}.{nameof(passwordsToGenerate)}", + defaultValue: 1); + + private enum GridColumn + { + Stretch + } + + private enum GridRow + { + Settings, + Results + } + + private readonly ISettingsProvider _settingsProvider; + private readonly IUIMultiLineTextInput _outputText = MultilineTextInput(); + private readonly IUISetting _excludedCharactersSetting = Setting(); + private readonly IUIInfoBar _infoBar = InfoBar(); + + [ImportingConstructor] + public PasswordGeneratorGuidTool(ISettingsProvider settingsProvider) + { + _settingsProvider = settingsProvider; + + OnGenerateButtonClick(); + } + + public UIToolView View + => new( + isScrollable: true, + Grid() + .ColumnLargeSpacing() + .RowLargeSpacing() + .Rows( + (GridRow.Settings, Auto), + (GridRow.Results, new UIGridLength(1, UIGridUnitType.Fraction))) + .Columns( + (GridColumn.Stretch, new UIGridLength(1, UIGridUnitType.Fraction))) + + .Cells( + Cell( + GridRow.Settings, + GridColumn.Stretch, + Stack() + .Vertical() + .LargeSpacing() + .WithChildren( + + Stack() + .Vertical() + .WithChildren( + + Label().Text(PasswordGenerator.ConfigurationTitle), + + SettingGroup() + .Icon("FluentSystemIcons", '\uF57D') + .Title(PasswordGenerator.Length) + .InteractiveElement( + NumberInput() + .Minimum(1) + .Value(_settingsProvider.GetSetting(length)) + .HideCommandBar() + .OnTextChanged(OnLengthChanged)) + .WithSettings( + + Setting() + .Title(PasswordGenerator.Lowercase) + .Description(PasswordGenerator.LowercaseDescription) + .Handle( + _settingsProvider, + lowercase, + stateDescriptionWhenOn: PasswordGenerator.LowercaseDescriptionStateOn, + stateDescriptionWhenOff: null, + OnSettingChanged), + + Setting() + .Title(PasswordGenerator.Uppercase) + .Description(PasswordGenerator.UppercaseDescription) + .Handle( + _settingsProvider, + uppercase, + stateDescriptionWhenOn: PasswordGenerator.UppercaseDescriptionStateOn, + stateDescriptionWhenOff: null, + OnSettingChanged), + + Setting() + .Title(PasswordGenerator.Digits) + .Description(PasswordGenerator.DigitsDescription) + .Handle( + _settingsProvider, + numbers, + stateDescriptionWhenOn: PasswordGenerator.DigitsDescriptionStateOn, + stateDescriptionWhenOff: null, + OnSettingChanged), + + Setting() + .Title(PasswordGenerator.SpecialCharacters) + .Description(PasswordGenerator.SpecialCharactersDescription) + .Handle( + _settingsProvider, + specialCharacters, + stateDescriptionWhenOn: PasswordGenerator.SpecialCharactersDescriptionStateOn, + stateDescriptionWhenOff: null, + OnSettingChanged), + + _excludedCharactersSetting + .Title(PasswordGenerator.ExcludeCharacters) + .StateDescription(GenerateExcludedCharactersDescriptionState()) + .InteractiveElement( + SingleLineTextInput() + .HideCommandBar() + .Text(_settingsProvider.GetSetting(excludedCharacters)) + .OnTextChanged(OnExcludedCharactersChanged))), + + _infoBar + .Warning() + .Description(PasswordGenerator.NoCharacterSetsWarning) + .NonClosable()), + + Stack() + .Vertical() + .WithChildren( + + Label().Text(PasswordGenerator.GenerateTitle), + Stack() + .Horizontal() + .SmallSpacing() + .WithChildren( + + Button() + .AccentAppearance() + .Text(PasswordGenerator.GenerateButton) + .OnClick(OnGenerateButtonClick), + + Label().Style(UILabelStyle.BodyStrong).Text(PasswordGenerator.MultiplySymbol), + + NumberInput() + .Minimum(1) + .Maximum(10000) + .Value(_settingsProvider.GetSetting(passwordsToGenerate)) + .HideCommandBar() + .OnTextChanged(OnNumberOfPasswordsToGenerateChanged))))), + + Cell( + GridRow.Results, + GridColumn.Stretch, + + _outputText + .Title(PasswordGenerator.Output) + .ReadOnly()))); + + private bool HasAnyCharacterSets + => _settingsProvider.GetSetting(uppercase) + || _settingsProvider.GetSetting(lowercase) + || _settingsProvider.GetSetting(numbers) + || _settingsProvider.GetSetting(specialCharacters); + + public void OnDataReceived(string dataTypeName, object? parsedData) + { + } + + private void OnSettingChanged(bool value) + { + OnGenerateButtonClick(); + } + + private void OnLengthChanged(string value) + { + if (int.TryParse(value, out int result)) + { + _settingsProvider.SetSetting(length, result); + } + else + { + _settingsProvider.SetSetting(length, 1); + } + + OnGenerateButtonClick(); + } + + private void OnExcludedCharactersChanged(string value) + { + _settingsProvider.SetSetting(excludedCharacters, value); + _excludedCharactersSetting.StateDescription(GenerateExcludedCharactersDescriptionState()); + + OnGenerateButtonClick(); + } + + private void OnNumberOfPasswordsToGenerateChanged(string value) + { + if (int.TryParse(value, out int result)) + { + _settingsProvider.SetSetting(passwordsToGenerate, result); + } + else + { + _settingsProvider.SetSetting(passwordsToGenerate, 1); + } + + OnGenerateButtonClick(); + } + + private void OnGenerateButtonClick() + { + // There are no character sets selected, so we can't generate anything. + if (!HasAnyCharacterSets) + { + _infoBar.Open(); + _outputText.Text(string.Empty); + return; + } + else + { + _infoBar.Close(); + } + + bool hasUppercase = _settingsProvider.GetSetting(uppercase); + bool hasLowercase = _settingsProvider.GetSetting(lowercase); + bool hasNumbers = _settingsProvider.GetSetting(numbers); + bool hasSpecialCharacters = _settingsProvider.GetSetting(specialCharacters); + char[] excludedCharactersList = _settingsProvider.GetSetting(excludedCharacters).ToCharArray(); + int passwordLength = _settingsProvider.GetSetting(length); + + // Generate a random password using the the combined character set. + var newPasswords = new StringBuilder(); + for (int i = 0; i < _settingsProvider.GetSetting(passwordsToGenerate); i++) + { + string password + = PasswordGeneratorHelper.GeneratePassword( + passwordLength, + hasUppercase, + hasLowercase, + hasNumbers, + hasSpecialCharacters, + excludedCharactersList); + + if (password.Length > 0) + { + newPasswords.AppendLine(password); + } + } + + _outputText.Text(newPasswords.ToString()); + } + + private string GenerateExcludedCharactersDescriptionState() + { + if (_settingsProvider.GetSetting(excludedCharacters).Length == 0) + { + return string.Empty; + } + else + { + return string.Format(PasswordGenerator.ExcludedCharactersDescriptionState, _settingsProvider.GetSetting(excludedCharacters)); + } + } +} From 1c9f4af0ed8de3f4dee617be5195bcc776d77341 Mon Sep 17 00:00:00 2001 From: Etienne Baudoux Date: Thu, 14 Dec 2023 22:37:30 -0800 Subject: [PATCH 2/2] Added unit tests --- .../Helpers/PasswordGeneratorHelper.cs | 42 ++++------ .../Password/PasswordGeneratorGuidTool.cs | 36 +++------ .../Helpers/PasswordGeneratorHelperTests.cs | 80 +++++++++++++++++++ 3 files changed, 107 insertions(+), 51 deletions(-) create mode 100644 src/app/tests/DevToys.UnitTests/Tools/Helpers/PasswordGeneratorHelperTests.cs diff --git a/src/app/dev/DevToys.Tools/Helpers/PasswordGeneratorHelper.cs b/src/app/dev/DevToys.Tools/Helpers/PasswordGeneratorHelper.cs index 09b0f5bbb1..b818946a2b 100644 --- a/src/app/dev/DevToys.Tools/Helpers/PasswordGeneratorHelper.cs +++ b/src/app/dev/DevToys.Tools/Helpers/PasswordGeneratorHelper.cs @@ -8,22 +8,22 @@ internal static class PasswordGeneratorHelper /// /// All non-alphanumeric characters. /// - private const string NonAlphanumeric = "!@#$%^&*"; + internal const string NonAlphanumeric = "!@#$%^&*"; /// /// All lower case ASCII characters. /// - private const string LowercaseLetters = "abcdefghijkmnopqrstuvwxyz"; + internal const string LowercaseLetters = "abcdefghijkmnopqrstuvwxyz"; /// /// All upper case ASCII characters. /// - private const string UppercaseLetters = "ABCDEFGHJKLMNOPQRSTUVWXYZ"; + internal const string UppercaseLetters = "ABCDEFGHJKLMNOPQRSTUVWXYZ"; /// /// All digits. /// - private const string Digits = "0123456789"; + internal const string Digits = "0123456789"; internal static string GeneratePassword( int length, @@ -31,8 +31,13 @@ internal static string GeneratePassword( bool hasLowercase, bool hasNumbers, bool hasSpecialCharacters, - char[] excludedCharacters) + char[]? excludedCharacters) { + if (length <= 0) + { + return string.Empty; + } + // Combine all character sets together. string[] randomChars = new[] { string.Empty, @@ -47,39 +52,21 @@ internal static string GeneratePassword( if (hasUppercase) { randomChars[0] = RemoveExcludedCharacters(UppercaseLetters, excludedCharacters); - - // If the whole set gets excluded don't include it. - if (randomChars[0].Length != 0) - { - newPasswordCharacters.Insert(rand.Next(0, newPasswordCharacters.Count), randomChars[0][rand.Next(0, randomChars[0].Length)]); - } } if (hasLowercase) { randomChars[1] = RemoveExcludedCharacters(LowercaseLetters, excludedCharacters); - if (randomChars[1].Length != 0) - { - newPasswordCharacters.Insert(rand.Next(0, newPasswordCharacters.Count), randomChars[1][rand.Next(0, randomChars[1].Length)]); - } } if (hasNumbers) { randomChars[2] = RemoveExcludedCharacters(Digits, excludedCharacters); - if (randomChars[2].Length != 0) - { - newPasswordCharacters.Insert(rand.Next(0, newPasswordCharacters.Count), randomChars[2][rand.Next(0, randomChars[2].Length)]); - } } if (hasSpecialCharacters) { randomChars[3] = RemoveExcludedCharacters(NonAlphanumeric, excludedCharacters); - if (randomChars[3].Length != 0) - { - newPasswordCharacters.Insert(rand.Next(0, newPasswordCharacters.Count), randomChars[3][rand.Next(0, randomChars[3].Length)]); - } } randomChars = randomChars.Where(r => r.Length > 0).ToArray(); @@ -87,7 +74,7 @@ internal static string GeneratePassword( // Only continue if the user hasn't excluded everything. if (randomChars.Length != 0) { - for (int j = newPasswordCharacters.Count; j < length; j++) + 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)]); @@ -97,8 +84,13 @@ internal static string GeneratePassword( return new string(newPasswordCharacters.ToArray()); } - private static string RemoveExcludedCharacters(string input, char[] excludedCharacters) + private static string RemoveExcludedCharacters(string input, char[]? excludedCharacters) { + if (excludedCharacters == null || excludedCharacters.Length == 0) + { + return input; + } + var excludedSet = new HashSet(excludedCharacters); // HashSet provides a faster lookup than Array.Contains(). var stringBuilder = new StringBuilder(); diff --git a/src/app/dev/DevToys.Tools/Tools/Generators/Password/PasswordGeneratorGuidTool.cs b/src/app/dev/DevToys.Tools/Tools/Generators/Password/PasswordGeneratorGuidTool.cs index f01816b898..8e7929d24f 100644 --- a/src/app/dev/DevToys.Tools/Tools/Generators/Password/PasswordGeneratorGuidTool.cs +++ b/src/app/dev/DevToys.Tools/Tools/Generators/Password/PasswordGeneratorGuidTool.cs @@ -129,10 +129,10 @@ public UIToolView View .Title(PasswordGenerator.Length) .InteractiveElement( NumberInput() - .Minimum(1) - .Value(_settingsProvider.GetSetting(length)) .HideCommandBar() - .OnTextChanged(OnLengthChanged)) + .Minimum(5) + .OnValueChanged(OnLengthChanged) + .Value(_settingsProvider.GetSetting(length))) .WithSettings( Setting() @@ -207,11 +207,11 @@ public UIToolView View Label().Style(UILabelStyle.BodyStrong).Text(PasswordGenerator.MultiplySymbol), NumberInput() + .HideCommandBar() .Minimum(1) .Maximum(10000) - .Value(_settingsProvider.GetSetting(passwordsToGenerate)) - .HideCommandBar() - .OnTextChanged(OnNumberOfPasswordsToGenerateChanged))))), + .OnValueChanged(OnNumberOfPasswordsToGenerateChanged) + .Value(_settingsProvider.GetSetting(passwordsToGenerate)))))), Cell( GridRow.Results, @@ -236,17 +236,9 @@ private void OnSettingChanged(bool value) OnGenerateButtonClick(); } - private void OnLengthChanged(string value) + private void OnLengthChanged(double value) { - if (int.TryParse(value, out int result)) - { - _settingsProvider.SetSetting(length, result); - } - else - { - _settingsProvider.SetSetting(length, 1); - } - + _settingsProvider.SetSetting(length, (int)value); OnGenerateButtonClick(); } @@ -258,17 +250,9 @@ private void OnExcludedCharactersChanged(string value) OnGenerateButtonClick(); } - private void OnNumberOfPasswordsToGenerateChanged(string value) + private void OnNumberOfPasswordsToGenerateChanged(double value) { - if (int.TryParse(value, out int result)) - { - _settingsProvider.SetSetting(passwordsToGenerate, result); - } - else - { - _settingsProvider.SetSetting(passwordsToGenerate, 1); - } - + _settingsProvider.SetSetting(passwordsToGenerate, (int)value); OnGenerateButtonClick(); } diff --git a/src/app/tests/DevToys.UnitTests/Tools/Helpers/PasswordGeneratorHelperTests.cs b/src/app/tests/DevToys.UnitTests/Tools/Helpers/PasswordGeneratorHelperTests.cs new file mode 100644 index 0000000000..2bf4ae6e15 --- /dev/null +++ b/src/app/tests/DevToys.UnitTests/Tools/Helpers/PasswordGeneratorHelperTests.cs @@ -0,0 +1,80 @@ +using DevToys.Tools.Helpers; + +namespace DevToys.UnitTests.Tools.Helpers; + +public class PasswordGeneratorHelperTests +{ + [Theory] + [InlineData(1, true, false, false, false, null)] + [InlineData(1, false, true, false, false, null)] + [InlineData(1, false, false, true, false, null)] + [InlineData(1, false, false, false, true, null)] + [InlineData(100, true, true, true, true, null)] + [InlineData(100, true, true, true, true, "bcdefghijklmnopqrstuvwxyz")] + [InlineData(100, false, true, false, false, "bcdefghijklmnopqrstuvwxyz")] + internal void GeneratePassword(int length, bool hasUppercase, bool hasLowercase, bool hasNumber, bool hasSpecialCharacters, string? excludedCharacters) + { + string password + = PasswordGeneratorHelper.GeneratePassword( + length, + hasUppercase, + hasLowercase, + hasNumber, + hasSpecialCharacters, + excludedCharacters?.ToCharArray()); + + password.Should().NotBeNullOrEmpty(); + password.Length.Should().Be(length); + + if (hasUppercase) + { + ContainAny(password, PasswordGeneratorHelper.UppercaseLetters); + } + else + { + NotContainAny(password, PasswordGeneratorHelper.UppercaseLetters); + } + + if (hasLowercase) + { + ContainAny(password, PasswordGeneratorHelper.LowercaseLetters); + } + else + { + NotContainAny(password, PasswordGeneratorHelper.LowercaseLetters); + } + + if (hasNumber) + { + ContainAny(password, PasswordGeneratorHelper.Digits); + } + else + { + NotContainAny(password, PasswordGeneratorHelper.Digits); + } + + if (hasSpecialCharacters) + { + ContainAny(password, PasswordGeneratorHelper.NonAlphanumeric); + } + else + { + NotContainAny(password, PasswordGeneratorHelper.NonAlphanumeric); + } + + if (excludedCharacters != null) + { + NotContainAny(password, excludedCharacters); + } + } + + private static void ContainAny(string password, string characters) + { + characters.Should().ContainAny(password.ToCharArray().Select(c => c.ToString())); + } + + private static void NotContainAny(string password, string characters) + { + characters.Should().NotContainAny(password.ToCharArray().Select(c => c.ToString())); + } +}