diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 69e9093..7637806 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-format": { - "version": "5.1.250801", + "version": "8.0.453106", "commands": [ "dotnet-format" ] diff --git a/.husky/task-runner.json b/.husky/task-runner.json index 17e70d9..80ad634 100644 --- a/.husky/task-runner.json +++ b/.husky/task-runner.json @@ -4,7 +4,7 @@ "name": "dotnet-format", "command": "dotnet", "group": "pre-commit", - "args": ["format", "--include" , "${staged}"], + "args": ["dotnet-format", "--include" , "${staged}"], "include": ["**/*.cs"] } ] diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000..baddbff --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/PwSafeClient.CLI/Commands/CommandHandler.cs b/src/PwSafeClient.CLI/Commands/CommandHandler.cs index a4e2b41..f696771 100644 --- a/src/PwSafeClient.CLI/Commands/CommandHandler.cs +++ b/src/PwSafeClient.CLI/Commands/CommandHandler.cs @@ -8,7 +8,7 @@ public class CommandHandler : ICommandHandler { public int Invoke(InvocationContext context) { - int result = 0; + var result = 0; Task.Run(async () => result = await InvokeAsync(context)).Wait(); return result; } diff --git a/src/PwSafeClient.CLI/Commands/ConfigManagement/InitConfigCommand.cs b/src/PwSafeClient.CLI/Commands/ConfigManagement/InitConfigCommand.cs index 973bebf..1edb932 100644 --- a/src/PwSafeClient.CLI/Commands/ConfigManagement/InitConfigCommand.cs +++ b/src/PwSafeClient.CLI/Commands/ConfigManagement/InitConfigCommand.cs @@ -27,7 +27,7 @@ public InitConfigCommandHandler(IConfigManager configManager, IConsoleService co public override async Task InvokeAsync(InvocationContext context) { - string filepath = configManager.GetConfigPath(); + var filepath = configManager.GetConfigPath(); if (configManager.ConfigExists()) { diff --git a/src/PwSafeClient.CLI/Commands/DatabaseManagement/CreateDbCommand.cs b/src/PwSafeClient.CLI/Commands/DatabaseManagement/CreateDbCommand.cs index cc60db9..3280b40 100644 --- a/src/PwSafeClient.CLI/Commands/DatabaseManagement/CreateDbCommand.cs +++ b/src/PwSafeClient.CLI/Commands/DatabaseManagement/CreateDbCommand.cs @@ -70,7 +70,7 @@ public override async Task InvokeAsync(InvocationContext context) try { - Document document = new Document(password); + var document = new Document(password); document.Save(File.FullName); await configManager.AddDatabaseAsync(Alias, File.FullName, Default); diff --git a/src/PwSafeClient.CLI/Commands/DatabaseManagement/ShowDbCommand.cs b/src/PwSafeClient.CLI/Commands/DatabaseManagement/ShowDbCommand.cs index 17db9e9..a2989af 100644 --- a/src/PwSafeClient.CLI/Commands/DatabaseManagement/ShowDbCommand.cs +++ b/src/PwSafeClient.CLI/Commands/DatabaseManagement/ShowDbCommand.cs @@ -38,14 +38,14 @@ public ShowDbCommandHandler(IDocumentHelper documentHelper, IConsoleService cons public override async Task InvokeAsync(InvocationContext context) { - Document? doc = await documentHelper.TryLoadDocumentAsync(Alias, File, true); + var doc = await documentHelper.TryLoadDocumentAsync(Alias, File, true); if (doc == null) { return 1; } - string format = "{0, -30}{1}"; + var format = "{0, -30}{1}"; Console.WriteLine(format, "Database UUID:", doc.Uuid); Console.WriteLine(format, "Name:", doc.Name ?? "-"); Console.WriteLine(format, "Description:", doc.Description ?? "-"); diff --git a/src/PwSafeClient.CLI/Commands/GetPasswordCommand.cs b/src/PwSafeClient.CLI/Commands/GetPasswordCommand.cs index 8379038..072cf76 100644 --- a/src/PwSafeClient.CLI/Commands/GetPasswordCommand.cs +++ b/src/PwSafeClient.CLI/Commands/GetPasswordCommand.cs @@ -43,7 +43,7 @@ public GetPasswordCommandHandler(IConsoleService consoleService, IDocumentHelper public override async Task InvokeAsync(InvocationContext context) { - Document? document = await documentHelper.TryLoadDocumentAsync(Alias, File, true); + var document = await documentHelper.TryLoadDocumentAsync(Alias, File, true); if (document == null) { return 1; diff --git a/src/PwSafeClient.CLI/Commands/ListEntriesCommand.cs b/src/PwSafeClient.CLI/Commands/ListEntriesCommand.cs index f6ea80f..6b75842 100644 --- a/src/PwSafeClient.CLI/Commands/ListEntriesCommand.cs +++ b/src/PwSafeClient.CLI/Commands/ListEntriesCommand.cs @@ -62,14 +62,14 @@ public ListEntriesCommandHandler(IDocumentHelper documentHelper, IConsoleService public override async Task InvokeAsync(InvocationContext context) { - Document? document = await documentHelper.TryLoadDocumentAsync(Alias, File, true); + var document = await documentHelper.TryLoadDocumentAsync(Alias, File, true); if (document == null) { return 1; } - List entries = document.Entries.ToList(); + var entries = document.Entries.ToList(); if (!string.IsNullOrEmpty(Filter)) { @@ -102,7 +102,7 @@ private static void PrintListView(List entries) Console.OutputEncoding = System.Text.Encoding.UTF8; Console.WriteLine(fmt, "Uuid", "Title", "Username", "Group"); - foreach (Entry entry in oderedEntries) + foreach (var entry in oderedEntries) { Console.WriteLine(fmt, entry.Uuid, entry.Title, entry.UserName, entry.Group); } @@ -111,13 +111,13 @@ private static void PrintListView(List entries) private static void PrintTreeView(List entries) { Console.OutputEncoding = System.Text.Encoding.UTF8; - Group root = new GroupBuilder(entries).Build(); + var root = new GroupBuilder(entries).Build(); PrintTreeView(entries, root, 0); } private static void PrintTreeView(List entries, Group group, int depth) { - GroupPath groupPath = group.GetGroupPath(); + var groupPath = group.GetGroupPath(); IEnumerable subEntries = entries .Where(entry => entry.Group.Equals(groupPath)) @@ -128,13 +128,13 @@ private static void PrintTreeView(List entries, Group group, int depth) Console.WriteLine("{0}|- {1}", new string(' ', (depth - 1) * 2), group.Name); } - foreach (Entry entry in subEntries) + foreach (var entry in subEntries) { var fmt = "{0}|- {1}({2}) [{3}]"; Console.WriteLine(fmt, new string(' ', depth * 2), entry.Title, entry.UserName, entry.Uuid); } - foreach (Group child in group.Children) + foreach (var child in group.Children) { PrintTreeView(entries, child, depth + 1); } diff --git a/src/PwSafeClient.CLI/Commands/NewEntryCommand.cs b/src/PwSafeClient.CLI/Commands/NewEntryCommand.cs index 2502a7d..9d03a8e 100644 --- a/src/PwSafeClient.CLI/Commands/NewEntryCommand.cs +++ b/src/PwSafeClient.CLI/Commands/NewEntryCommand.cs @@ -104,7 +104,7 @@ public NewEntryCommandHandler(IConsoleService consoleService, IDocumentHelper do public override async Task InvokeAsync(InvocationContext context) { - Document? document = await documentHelper.TryLoadDocumentAsync(Alias, File, false); + var document = await documentHelper.TryLoadDocumentAsync(Alias, File, false); if (document == null) { return 1; @@ -126,7 +126,7 @@ public override async Task InvokeAsync(InvocationContext context) if (!string.IsNullOrWhiteSpace(Group)) { groupSegments = Group.Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries); - for (int i = 0; i < groupSegments.Length; i++) + for (var i = 0; i < groupSegments.Length; i++) { groupSegments[i] = groupSegments[i].Trim(); } @@ -136,7 +136,7 @@ public override async Task InvokeAsync(InvocationContext context) if (document.Entries.Any(e => e.Title == Title && e.Group.Equals(targetGroupPath))) { consoleService.LogError($"The entry {Title} already exists under the group {Group}."); - bool shouldContinue = consoleService.DoConfirm("Do you want to continue?"); + var shouldContinue = consoleService.DoConfirm("Do you want to continue?"); if (!shouldContinue) { @@ -156,7 +156,7 @@ public override async Task InvokeAsync(InvocationContext context) Notes = Notes }; - string newPassword = string.Empty; + var newPassword = string.Empty; if (context.ParseResult.HasOption(PasswordOption)) { diff --git a/src/PwSafeClient.CLI/Commands/PolicyManagement/AddPolicyCommand.cs b/src/PwSafeClient.CLI/Commands/PolicyManagement/AddPolicyCommand.cs index bcfa500..7867f02 100644 --- a/src/PwSafeClient.CLI/Commands/PolicyManagement/AddPolicyCommand.cs +++ b/src/PwSafeClient.CLI/Commands/PolicyManagement/AddPolicyCommand.cs @@ -85,7 +85,15 @@ public override async Task InvokeAsync(InvocationContext context) return 1; } - Document? document = await documentHelper.TryLoadDocumentAsync(Alias, File, false); + if (HexOnly && (Digits > 0 || Uppercase > 0 || Lowercase > 0 || Symbols > 0)) + { + if (!consoleService.DoConfirm("When --hex-ony is set, all other options will be ignored, continue?")) + { + return 0; + } + } + + var document = await documentHelper.TryLoadDocumentAsync(Alias, File, false); if (document == null) { return 1; @@ -101,61 +109,78 @@ public override async Task InvokeAsync(InvocationContext context) var policy = new NamedPasswordPolicy(Name, Length); PasswordPolicyStyle style = 0; + var minimumDigitCount = 0; + var minimumUppercaseCount = 0; + var minimumLowercaseCount = 0; + var minimumSymbolCount = 0; if (HexOnly) { style |= PasswordPolicyStyle.UseHexDigits; } - - if (EasyVision) + else { - style |= PasswordPolicyStyle.UseEasyVision; - } + if (Pronounceable) + { + style |= PasswordPolicyStyle.MakePronounceable; + } - if (Pronounceable) - { - style |= PasswordPolicyStyle.MakePronounceable; - } + if (EasyVision) + { + style |= PasswordPolicyStyle.UseEasyVision; + } - if (Digits >= 0) - { - style |= PasswordPolicyStyle.UseDigits; - } + if (Digits >= 0) + { + style |= PasswordPolicyStyle.UseDigits; + minimumDigitCount = Pronounceable ? 0 : Digits; + } - if (Uppercase >= 0) - { - style |= PasswordPolicyStyle.UseUppercase; - } + if (Uppercase >= 0) + { + style |= PasswordPolicyStyle.UseUppercase; + minimumUppercaseCount = Pronounceable ? 0 : Uppercase; + } - if (Lowercase >= 0) - { - style |= PasswordPolicyStyle.UseLowercase; - } + if (Lowercase >= 0) + { + style |= PasswordPolicyStyle.UseLowercase; + minimumLowercaseCount = Pronounceable ? 0 : Lowercase; + } - if (Symbols >= 0) - { - style |= PasswordPolicyStyle.UseSymbols; + if (Symbols >= 0) + { + style |= PasswordPolicyStyle.UseSymbols; + minimumSymbolCount = Pronounceable ? 0 : Symbols; + } } policy.Style = style; - policy.MinimumDigitCount = FilterNegativeValue(Digits); - policy.MinimumUppercaseCount = FilterNegativeValue(Uppercase); - policy.MinimumLowercaseCount = FilterNegativeValue(Lowercase); - policy.MinimumSymbolCount = FilterNegativeValue(Symbols); + policy.MinimumDigitCount = minimumDigitCount; + policy.MinimumUppercaseCount = minimumUppercaseCount; + policy.MinimumLowercaseCount = minimumLowercaseCount; + policy.MinimumSymbolCount = minimumSymbolCount; - if (!string.IsNullOrWhiteSpace(SymbolChars)) + if (Pronounceable) { - policy.SetSpecialSymbolSet(SymbolChars.ToArray()); + policy.SetSpecialSymbolSet(PwCharPool.PronounceableSymbolChars); } else { - if (policy.Style.HasFlag(PasswordPolicyStyle.UseEasyVision)) + if (!string.IsNullOrWhiteSpace(SymbolChars)) { - policy.SetSpecialSymbolSet(PwCharPool.EasyVisionSymbolChars); + policy.SetSpecialSymbolSet(SymbolChars.ToArray()); } else { - policy.SetSpecialSymbolSet(PwCharPool.StdSymbolChars); + if (EasyVision) + { + policy.SetSpecialSymbolSet(PwCharPool.EasyVisionSymbolChars); + } + else + { + policy.SetSpecialSymbolSet(PwCharPool.StdSymbolChars); + } } } @@ -186,7 +211,7 @@ private bool HasValidatedInput() return false; } - int constraintsLength = FilterNegativeValue(Digits) + FilterNegativeValue(Uppercase) + FilterNegativeValue(Lowercase) + FilterNegativeValue(Symbols); + var constraintsLength = FilterNegativeValue(Digits) + FilterNegativeValue(Uppercase) + FilterNegativeValue(Lowercase) + FilterNegativeValue(Symbols); if (constraintsLength > Length) { @@ -194,6 +219,12 @@ private bool HasValidatedInput() return false; } + if (EasyVision && Pronounceable) + { + Console.WriteLine("The options '--easy-vision' and '--pronounceable' cannot be used together."); + return false; + } + if (!string.IsNullOrWhiteSpace(SymbolChars)) { if (PwCharPool.HasDuplicatedCharacters(SymbolChars)) diff --git a/src/PwSafeClient.CLI/Commands/PolicyManagement/GeneratePasswordCommand.cs b/src/PwSafeClient.CLI/Commands/PolicyManagement/GeneratePasswordCommand.cs new file mode 100644 index 0000000..d30e35a --- /dev/null +++ b/src/PwSafeClient.CLI/Commands/PolicyManagement/GeneratePasswordCommand.cs @@ -0,0 +1,83 @@ +using System; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +using Medo.Security.Cryptography.PasswordSafe; + +using PwSafeClient.CLI.Contracts.Helpers; +using PwSafeClient.CLI.Contracts.Services; +using PwSafeClient.CLI.Options; +using PwSafeClient.Shared; + +namespace PwSafeClient.CLI.Commands; + +internal class GeneratePasswordCommand : Command +{ + public GeneratePasswordCommand() : base("genpass", "Generate a new password") + { + AddOption(new Option(new[] { "--name", "-n" }, "Name of the policy to use")); + + AddOption(CommonOptions.AliasOption()); + AddOption(CommonOptions.FileOption()); + } + + public class GeneratePasswordCommandHandler : CommandHandler + { + private readonly IConsoleService consoleService; + private readonly IDocumentHelper documentHelper; + + public GeneratePasswordCommandHandler(IConsoleService consoleService, IDocumentHelper documentHelper) + { + this.consoleService = consoleService; + this.documentHelper = documentHelper; + } + + public string? Alias { get; set; } + public string? Name { get; set; } + public FileInfo? File { get; set; } + + public override async Task InvokeAsync(InvocationContext context) + { + var document = await documentHelper.TryLoadDocumentAsync(Alias, File, true); + if (document == null) + { + return 1; + } + + var namedPolicy = document.NamedPasswordPolicies.FirstOrDefault(p => p.Name == Name); + if (namedPolicy == null) + { + consoleService.LogError($"Policy '{Name}' not found."); + return 1; + } + + var policy = new PasswordPolicy(namedPolicy.TotalPasswordLength); + policy.Style = namedPolicy.Style; + policy.MinimumLowercaseCount = namedPolicy.MinimumLowercaseCount; + policy.MinimumUppercaseCount = namedPolicy.MinimumUppercaseCount; + policy.MinimumDigitCount = namedPolicy.MinimumDigitCount; + policy.MinimumSymbolCount = namedPolicy.MinimumSymbolCount; + policy.SetSpecialSymbolSet(namedPolicy.GetSpecialSymbolSet()); + + var generator = new PasswordGenerator(policy); + var password = generator.GeneratePassword(); + + if (string.IsNullOrEmpty(password)) + { + consoleService.LogError("Failed to generate a password."); + return 1; + } + else + { + consoleService.LogSuccess(password); + await TextCopy.ClipboardService.SetTextAsync(password); + Console.WriteLine("Password copied to clipboard."); + } + + return 0; + } + } +} diff --git a/src/PwSafeClient.CLI/Commands/PolicyManagement/ListPoliciesCommand.cs b/src/PwSafeClient.CLI/Commands/PolicyManagement/ListPoliciesCommand.cs index d16c03a..d793ad3 100644 --- a/src/PwSafeClient.CLI/Commands/PolicyManagement/ListPoliciesCommand.cs +++ b/src/PwSafeClient.CLI/Commands/PolicyManagement/ListPoliciesCommand.cs @@ -10,6 +10,7 @@ using PwSafeClient.CLI.Contracts.Helpers; using PwSafeClient.CLI.Contracts.Services; using PwSafeClient.CLI.Options; +using PwSafeClient.Shared; namespace PwSafeClient.CLI.Commands; @@ -39,7 +40,7 @@ public ListPoliciesCommandHandler(IConsoleService consoleService, IDocumentHelpe public override async Task InvokeAsync(InvocationContext context) { - Document? document = await documentHelper.TryLoadDocumentAsync(Alias, File, true); + var document = await documentHelper.TryLoadDocumentAsync(Alias, File, true); if (document == null) { return 1; @@ -49,7 +50,7 @@ public override async Task InvokeAsync(InvocationContext context) { if (document.NamedPasswordPolicies.Any()) { - foreach (NamedPasswordPolicy policy in document.NamedPasswordPolicies) + foreach (var policy in document.NamedPasswordPolicies) { PrintPasswordPolicy(policy); } @@ -69,58 +70,58 @@ public override async Task InvokeAsync(InvocationContext context) } } - private void PrintPasswordPolicy(NamedPasswordPolicy policy) + private static void PrintPasswordPolicy(NamedPasswordPolicy policy) { - bool isPronounceable = policy.Style.HasFlag(PasswordPolicyStyle.MakePronounceable); - bool useLowercase = policy.Style.HasFlag(PasswordPolicyStyle.UseLowercase); - bool useUppercase = policy.Style.HasFlag(PasswordPolicyStyle.UseUppercase); - bool useDigits = policy.Style.HasFlag(PasswordPolicyStyle.UseDigits); - bool useSymbols = policy.Style.HasFlag(PasswordPolicyStyle.UseSymbols); - - Console.WriteLine($"{policy.Name}:"); - Console.WriteLine($" Password length: {policy.TotalPasswordLength}."); - - if (policy.Style.HasFlag(PasswordPolicyStyle.UseHexDigits)) - { - Console.WriteLine($" Hex digits only."); - return; - } - - Console.WriteLine($" Pronounceable: {isPronounceable}."); - Console.WriteLine($" Easy vision: {policy.Style.HasFlag(PasswordPolicyStyle.UseEasyVision)}."); - - Console.WriteLine(); - Console.WriteLine($" Use lowercase: {useLowercase}."); - if (useLowercase && !isPronounceable) - { - Console.WriteLine($" Minimum lowercase: {policy.MinimumLowercaseCount}."); - } - - Console.WriteLine(); - Console.WriteLine($" Use uppercase: {useUppercase}."); - if (useUppercase && !isPronounceable) + var isPronounceable = policy.Style.HasFlag(PasswordPolicyStyle.MakePronounceable); + var useLowercase = policy.Style.HasFlag(PasswordPolicyStyle.UseLowercase); + var useUppercase = policy.Style.HasFlag(PasswordPolicyStyle.UseUppercase); + var useDigits = policy.Style.HasFlag(PasswordPolicyStyle.UseDigits); + var useSymbols = policy.Style.HasFlag(PasswordPolicyStyle.UseSymbols); + var useHexDigits = policy.Style.HasFlag(PasswordPolicyStyle.UseHexDigits); + var useEasyVision = policy.Style.HasFlag(PasswordPolicyStyle.UseEasyVision); + var symbols = policy.GetSpecialSymbolSet(); + + if (symbols.Length == 0 && !isPronounceable) { - Console.WriteLine($" Minimum uppercase: {policy.MinimumUppercaseCount}."); + symbols = useEasyVision ? PwCharPool.EasyVisionDigitChars : PwCharPool.StdDigitChars; } + var format = "{0,-30}: {1}"; + + Console.WriteLine($"---------------- {policy.Name} ----------------"); + Console.WriteLine(format, $"Password length", policy.TotalPasswordLength); + Console.WriteLine(format, "Use lowercase characters", RenderPolicyValue(useLowercase, isPronounceable ? 0 : policy.MinimumLowercaseCount)); + Console.WriteLine(format, "Use uppercase characters", RenderPolicyValue(useUppercase, isPronounceable ? 0 : policy.MinimumUppercaseCount)); + Console.WriteLine(format, "Use digits", RenderPolicyValue(useDigits, isPronounceable ? 0 : policy.MinimumDigitCount)); + Console.WriteLine(format, "Use symbols", RenderPolicyValue(useSymbols, isPronounceable ? 0 : policy.MinimumSymbolCount, string.Join("", symbols))); + Console.WriteLine(format, "Use easy vision characters", RenderPolicyValue(useHexDigits, 0)); + Console.WriteLine(format, "Pronounceable passwords", RenderPolicyValue(isPronounceable, 0)); + Console.WriteLine(format, "Hexadecimal characters", RenderPolicyValue(useHexDigits, 0)); Console.WriteLine(); - Console.WriteLine($" Use digits: {useDigits}."); - if (useDigits && !isPronounceable) - { - Console.WriteLine($" Minimum digits: {policy.MinimumDigitCount}."); - } + } - Console.WriteLine(); - if (!isPronounceable) + private static string RenderPolicyValue(bool enabled, int minimumCount, string? symbols = null) + { + if (enabled) { - Console.WriteLine($" Use symbols: {useSymbols}."); + if (minimumCount > 0) + { + if (!string.IsNullOrWhiteSpace(symbols)) + { + return $"Yes - (At least {minimumCount}) from set '{symbols}"; + } - if (useSymbols) + return $"Yes - (At least {minimumCount})"; + } + else { - Console.WriteLine($" Minimum symbols: {policy.MinimumSymbolCount}."); - Console.WriteLine($" Symbols: {policy.GetSpecialSymbolSet()}."); + return "Yes"; } } + else + { + return "No"; + } } } } diff --git a/src/PwSafeClient.CLI/Commands/PolicyManagement/PolicyCommand.cs b/src/PwSafeClient.CLI/Commands/PolicyManagement/PolicyCommand.cs index 955cc35..00160ee 100644 --- a/src/PwSafeClient.CLI/Commands/PolicyManagement/PolicyCommand.cs +++ b/src/PwSafeClient.CLI/Commands/PolicyManagement/PolicyCommand.cs @@ -10,5 +10,6 @@ public PolicyCommand() : base("policy", "Manage your password policies") AddCommand(new AddPolicyCommand()); AddCommand(new RemovePolicyCommand()); AddCommand(new UpdatePolicyCommand()); + AddCommand(new GeneratePasswordCommand()); } } diff --git a/src/PwSafeClient.CLI/Commands/PolicyManagement/RemovePolicyCommand.cs b/src/PwSafeClient.CLI/Commands/PolicyManagement/RemovePolicyCommand.cs index 0119e99..fa91175 100644 --- a/src/PwSafeClient.CLI/Commands/PolicyManagement/RemovePolicyCommand.cs +++ b/src/PwSafeClient.CLI/Commands/PolicyManagement/RemovePolicyCommand.cs @@ -43,7 +43,7 @@ public RemovePolicyCommandHandler(IConsoleService consoleService, IDocumentHelpe public override async Task InvokeAsync(InvocationContext context) { - Document? document = await documentHelper.TryLoadDocumentAsync(Alias, File, false); + var document = await documentHelper.TryLoadDocumentAsync(Alias, File, false); if (document == null) { diff --git a/src/PwSafeClient.CLI/Commands/PolicyManagement/UpdatePolicyCommand.cs b/src/PwSafeClient.CLI/Commands/PolicyManagement/UpdatePolicyCommand.cs index 8fcff76..d32236c 100644 --- a/src/PwSafeClient.CLI/Commands/PolicyManagement/UpdatePolicyCommand.cs +++ b/src/PwSafeClient.CLI/Commands/PolicyManagement/UpdatePolicyCommand.cs @@ -85,7 +85,15 @@ public override async Task InvokeAsync(InvocationContext context) return 1; } - Document? document = await documentHelper.TryLoadDocumentAsync(Alias, File, false); + if (HexOnly && (Digits > 0 || Uppercase > 0 || Lowercase > 0 || Symbols > 0)) + { + if (!consoleService.DoConfirm("When --hex-ony is set, all other options will be ignored, continue?")) + { + return 0; + } + } + + var document = await documentHelper.TryLoadDocumentAsync(Alias, File, false); if (document == null) { return 1; @@ -104,61 +112,78 @@ public override async Task InvokeAsync(InvocationContext context) policy.TotalPasswordLength = Length; PasswordPolicyStyle style = 0; + var minimumDigitCount = 0; + var minimumUppercaseCount = 0; + var minimumLowercaseCount = 0; + var minimumSymbolCount = 0; if (HexOnly) { style |= PasswordPolicyStyle.UseHexDigits; } - - if (EasyVision) + else { - style |= PasswordPolicyStyle.UseEasyVision; - } + if (Pronounceable) + { + style |= PasswordPolicyStyle.MakePronounceable; + } - if (Pronounceable) - { - style |= PasswordPolicyStyle.MakePronounceable; - } + if (EasyVision) + { + style |= PasswordPolicyStyle.UseEasyVision; + } - if (Digits >= 0) - { - style |= PasswordPolicyStyle.UseDigits; - } + if (Digits >= 0) + { + style |= PasswordPolicyStyle.UseDigits; + minimumDigitCount = Pronounceable ? 0 : Digits; + } - if (Uppercase >= 0) - { - style |= PasswordPolicyStyle.UseUppercase; - } + if (Uppercase >= 0) + { + style |= PasswordPolicyStyle.UseUppercase; + minimumUppercaseCount = Pronounceable ? 0 : Uppercase; + } - if (Lowercase >= 0) - { - style |= PasswordPolicyStyle.UseLowercase; - } + if (Lowercase >= 0) + { + style |= PasswordPolicyStyle.UseLowercase; + minimumLowercaseCount = Pronounceable ? 0 : Lowercase; + } - if (Symbols >= 0) - { - style |= PasswordPolicyStyle.UseSymbols; + if (Symbols >= 0) + { + style |= PasswordPolicyStyle.UseSymbols; + minimumSymbolCount = Pronounceable ? 0 : Symbols; + } } policy.Style = style; - policy.MinimumDigitCount = FilterNegativeValue(Digits); - policy.MinimumUppercaseCount = FilterNegativeValue(Uppercase); - policy.MinimumLowercaseCount = FilterNegativeValue(Lowercase); - policy.MinimumSymbolCount = FilterNegativeValue(Symbols); + policy.MinimumDigitCount = minimumDigitCount; + policy.MinimumUppercaseCount = minimumUppercaseCount; + policy.MinimumLowercaseCount = minimumLowercaseCount; + policy.MinimumSymbolCount = minimumSymbolCount; - if (!string.IsNullOrWhiteSpace(SymbolChars)) + if (Pronounceable) { - policy.SetSpecialSymbolSet(SymbolChars.ToArray()); + policy.SetSpecialSymbolSet(PwCharPool.PronounceableSymbolChars); } else { - if (policy.Style.HasFlag(PasswordPolicyStyle.UseEasyVision)) + if (!string.IsNullOrWhiteSpace(SymbolChars)) { - policy.SetSpecialSymbolSet(PwCharPool.EasyVisionSymbolChars); + policy.SetSpecialSymbolSet(SymbolChars.ToArray()); } else { - policy.SetSpecialSymbolSet(PwCharPool.StdSymbolChars); + if (EasyVision) + { + policy.SetSpecialSymbolSet(PwCharPool.EasyVisionSymbolChars); + } + else + { + policy.SetSpecialSymbolSet(PwCharPool.StdSymbolChars); + } } } @@ -187,7 +212,7 @@ private bool HasValidatedInput() return false; } - int constraintsLength = FilterNegativeValue(Digits) + FilterNegativeValue(Uppercase) + FilterNegativeValue(Lowercase) + FilterNegativeValue(Symbols); + var constraintsLength = FilterNegativeValue(Digits) + FilterNegativeValue(Uppercase) + FilterNegativeValue(Lowercase) + FilterNegativeValue(Symbols); if (constraintsLength > Length) { @@ -195,6 +220,12 @@ private bool HasValidatedInput() return false; } + if (EasyVision && Pronounceable) + { + Console.WriteLine("The options '--easy-vision' and '--pronounceable' cannot be used together."); + return false; + } + if (!string.IsNullOrWhiteSpace(SymbolChars)) { if (PwCharPool.HasDuplicatedCharacters(SymbolChars)) diff --git a/src/PwSafeClient.CLI/Commands/RemoveEntryCommand.cs b/src/PwSafeClient.CLI/Commands/RemoveEntryCommand.cs index 928b7d6..849fa1b 100644 --- a/src/PwSafeClient.CLI/Commands/RemoveEntryCommand.cs +++ b/src/PwSafeClient.CLI/Commands/RemoveEntryCommand.cs @@ -52,8 +52,8 @@ public RemoveEntryCommandHandler(IConsoleService consoleService, IDocumentHelper public override async Task InvokeAsync(InvocationContext context) { - Document? document = await documentHelper.TryLoadDocumentAsync(Alias, File, true); - int totalRemovedEntries = 0; + var document = await documentHelper.TryLoadDocumentAsync(Alias, File, true); + var totalRemovedEntries = 0; if (document == null) { @@ -92,19 +92,19 @@ public override async Task InvokeAsync(InvocationContext context) } else if (!string.IsNullOrWhiteSpace(Group)) { - Group root = new GroupBuilder([.. document.Entries]).Build(); + var root = new GroupBuilder([.. document.Entries]).Build(); string[] groupSegments = []; if (!string.IsNullOrWhiteSpace(Group)) { groupSegments = Group.Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries); - for (int i = 0; i < groupSegments.Length; i++) + for (var i = 0; i < groupSegments.Length; i++) { groupSegments[i] = groupSegments[i].Trim(); } } - Group? targetGroup = root.GetSubGroupBySegments(groupSegments); + var targetGroup = root.GetSubGroupBySegments(groupSegments); if (targetGroup == null) { @@ -117,7 +117,7 @@ public override async Task InvokeAsync(InvocationContext context) List queue = [targetGroup]; while (queue.Count > 0) { - Group currentGroup = queue[0]; + var currentGroup = queue[0]; queue.RemoveAt(0); queue.AddRange(currentGroup.Children); targetItems.AddRange(document.Entries.Where(entry => entry.Group.Equals(currentGroup.GetGroupPath()))); @@ -128,7 +128,7 @@ public override async Task InvokeAsync(InvocationContext context) if (consoleService.DoConfirm($"Are you sure to remove {targetItems.Count} entries under group '{Group}'?")) { totalRemovedEntries = targetItems.Count; - foreach (Entry entry in targetItems) + foreach (var entry in targetItems) { document.Entries.Remove(entry); } diff --git a/src/PwSafeClient.CLI/Commands/RenewPasswordCommand.cs b/src/PwSafeClient.CLI/Commands/RenewPasswordCommand.cs index aad7335..618ea90 100644 --- a/src/PwSafeClient.CLI/Commands/RenewPasswordCommand.cs +++ b/src/PwSafeClient.CLI/Commands/RenewPasswordCommand.cs @@ -6,8 +6,6 @@ using System.Linq; using System.Threading.Tasks; -using Medo.Security.Cryptography.PasswordSafe; - using PwSafeClient.CLI.Contracts.Helpers; using PwSafeClient.CLI.Contracts.Services; using PwSafeClient.CLI.Options; @@ -59,8 +57,8 @@ public RenewPasswordCommandHandler(IConsoleService consoleService, IDocumentHelp public override async Task InvokeAsync(InvocationContext context) { - Document? document = await documentHelper.TryLoadDocumentAsync(Alias, File, true); - string newPassword = string.Empty; + var document = await documentHelper.TryLoadDocumentAsync(Alias, File, true); + var newPassword = string.Empty; if (document == null) { @@ -95,7 +93,7 @@ public override async Task InvokeAsync(InvocationContext context) if (string.IsNullOrEmpty(newPassword) && !string.IsNullOrWhiteSpace(Policy)) { - NamedPasswordPolicy? namedPasswordPolicy = document.NamedPasswordPolicies.FirstOrDefault(p => p.Name == Policy); + var namedPasswordPolicy = document.NamedPasswordPolicies.FirstOrDefault(p => p.Name == Policy); if (namedPasswordPolicy == null) { diff --git a/src/PwSafeClient.CLI/Commands/UnlockCommand.cs b/src/PwSafeClient.CLI/Commands/UnlockCommand.cs index 7acee02..6c97e5b 100644 --- a/src/PwSafeClient.CLI/Commands/UnlockCommand.cs +++ b/src/PwSafeClient.CLI/Commands/UnlockCommand.cs @@ -63,15 +63,15 @@ public UnlockCommandHandler(IDocumentHelper documentHelper, IConsoleService cons public override async Task InvokeAsync(InvocationContext context) { - Document? doc = await documentHelper.TryLoadDocumentAsync(Alias, File, ReadOnly); + var doc = await documentHelper.TryLoadDocumentAsync(Alias, File, ReadOnly); if (doc == null) { return 1; } - int idleTime = await configManager.GetIdleTimeAsync(); - string displayName = await documentHelper.GetDocumentDisplayNameAsync(Alias, File); + var idleTime = await configManager.GetIdleTimeAsync(); + var displayName = await documentHelper.GetDocumentDisplayNameAsync(Alias, File); Timer timer = new(_ => HandleExit(), null, (int)TimeSpan.FromMinutes(idleTime).TotalMilliseconds, Timeout.Infinite); Console.CancelKeyPress += (_, _) => HandleExit(); @@ -92,7 +92,7 @@ public override async Task InvokeAsync(InvocationContext context) break; } - ParseResult result = Program.Parser!.Parse(input); + var result = Program.Parser!.Parse(input); if (!allowedCommands.Contains(result.CommandResult.Command.Name)) { diff --git a/src/PwSafeClient.CLI/Commands/UpdateEntryCommand.cs b/src/PwSafeClient.CLI/Commands/UpdateEntryCommand.cs index 020a3e7..7b329ee 100644 --- a/src/PwSafeClient.CLI/Commands/UpdateEntryCommand.cs +++ b/src/PwSafeClient.CLI/Commands/UpdateEntryCommand.cs @@ -79,8 +79,8 @@ public UpdateEntryCommandHandler(IConsoleService consoleService, IDocumentHelper public override async Task InvokeAsync(InvocationContext context) { - Document? document = await documentHelper.TryLoadDocumentAsync(Alias, File, true); - bool isUpdated = false; + var document = await documentHelper.TryLoadDocumentAsync(Alias, File, true); + var isUpdated = false; if (document == null) { diff --git a/src/PwSafeClient.CLI/Helpers/DocumentHelper.cs b/src/PwSafeClient.CLI/Helpers/DocumentHelper.cs index e79cbdd..764dcf1 100644 --- a/src/PwSafeClient.CLI/Helpers/DocumentHelper.cs +++ b/src/PwSafeClient.CLI/Helpers/DocumentHelper.cs @@ -31,7 +31,7 @@ public DocumentHelper(IConfigManager configManager, IConsoleService consoleServi try { - string filepath = await GetDocumentFilePathAsync(alias, fileInfo); + var filepath = await GetDocumentFilePathAsync(alias, fileInfo); if (!File.Exists(filepath)) { @@ -39,7 +39,7 @@ public DocumentHelper(IConfigManager configManager, IConsoleService consoleServi return null; } - string password = consoleService.ReadPassword(); + var password = consoleService.ReadPassword(); document = Document.Load(filepath, password); document.IsReadOnly = readOnly; @@ -79,7 +79,7 @@ public async Task SaveDocumentAsync(string? alias, FileInfo? fileInfo) try { - string filepath = await GetDocumentFilePathAsync(alias, fileInfo); + var filepath = await GetDocumentFilePathAsync(alias, fileInfo); await BackupDocumentAsync(filepath); document.Save(filepath); } @@ -96,7 +96,7 @@ public async Task GetDocumentDisplayNameAsync(string? alias, FileInfo? f return fileInfo.Name; } - Config config = await configManager.LoadConfigAsync(); + var config = await configManager.LoadConfigAsync(); return alias ?? config.DefaultDatabase ?? string.Empty; } @@ -118,22 +118,22 @@ private async Task BackupDocumentAsync(string filepath) return; } - string? targetFolder = Path.GetDirectoryName(filepath); - string? filename = Path.GetFileNameWithoutExtension(filepath); - string? extension = Path.GetExtension(filepath); + var targetFolder = Path.GetDirectoryName(filepath); + var filename = Path.GetFileNameWithoutExtension(filepath); + var extension = Path.GetExtension(filepath); if (targetFolder == null || filename == null) { return; } - string[] backupFiles = Directory.GetFiles(targetFolder, $"{filename}_*.ibak") + var backupFiles = Directory.GetFiles(targetFolder, $"{filename}_*.ibak") .OrderByDescending(GetBackupVersion) .ToArray(); - int maxBackupCount = await configManager.GetMaxBackupCountAsync(); + var maxBackupCount = await configManager.GetMaxBackupCountAsync(); - int backupVersion = 1; + var backupVersion = 1; if (backupFiles.Length > 0) { @@ -146,7 +146,7 @@ private async Task BackupDocumentAsync(string filepath) if (backupFiles.Length >= maxBackupCount) { - for (int i = maxBackupCount - 1; i < backupFiles.Length; i++) + for (var i = maxBackupCount - 1; i < backupFiles.Length; i++) { File.Delete(backupFiles[i]); } @@ -155,18 +155,18 @@ private async Task BackupDocumentAsync(string filepath) private int GetBackupVersion(string filepath) { - string? filename = Path.GetFileNameWithoutExtension(filepath); - string? extension = Path.GetExtension(filepath); + var filename = Path.GetFileNameWithoutExtension(filepath); + var extension = Path.GetExtension(filepath); if (filename == null || extension == null) { return 0; } - string[]? nameAndVersionPair = filename.Split('_'); + var nameAndVersionPair = filename.Split('_'); if (nameAndVersionPair != null && nameAndVersionPair.Length == 2) { - if (int.TryParse(nameAndVersionPair[1], out int version)) + if (int.TryParse(nameAndVersionPair[1], out var version)) { return version; } diff --git a/src/PwSafeClient.CLI/Options/PasswordPolicyOptions.cs b/src/PwSafeClient.CLI/Options/PasswordPolicyOptions.cs index 20333f7..0bd373c 100644 --- a/src/PwSafeClient.CLI/Options/PasswordPolicyOptions.cs +++ b/src/PwSafeClient.CLI/Options/PasswordPolicyOptions.cs @@ -12,22 +12,22 @@ internal static class PasswordPolicyOptions public static Option DigitsOption() => new( aliases: ["--digits", "-d"], - description: "Use digits, pass '-1' to disable the option", + description: "Use digits and specify the minimum requirement, pass '-1' to disable the option", getDefaultValue: () => 0); public static Option UppercaseOption() => new( aliases: ["--uppercase", "-u"], - description: "Use uppercase, pass '-1' to disable the option", + description: "Use uppercase and specify the minimum requirement, pass '-1' to disable the option", getDefaultValue: () => 0); public static Option LowercaseOption() => new( aliases: ["--lowercase", "-l"], - description: "Use lowercase, pass '-1' to disable the option", + description: "Use lowercase and specify the minimum requirement, pass '-1' to disable the option", getDefaultValue: () => 0); public static Option SymbolsOption() => new( aliases: ["--symbols", "-s"], - description: "Use symbols, pass '-1' to disable the option", + description: "Use symbols and specify the minimum requirement, pass '-1' to disable the option", getDefaultValue: () => 0); public static Option SymbolCharsOption() => new( diff --git a/src/PwSafeClient.CLI/Program.cs b/src/PwSafeClient.CLI/Program.cs index 029dff9..d5a755d 100644 --- a/src/PwSafeClient.CLI/Program.cs +++ b/src/PwSafeClient.CLI/Program.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using PwSafeClient.CLI.Commands; using PwSafeClient.CLI.Contracts.Helpers; @@ -25,6 +26,15 @@ static void Main(string[] args) .UseHost(_ => Host.CreateDefaultBuilder(), host => { + host.ConfigureLogging(logger => + { +#if DEBUG + logger.SetMinimumLevel(LogLevel.Debug); +#else + logger.SetMinimumLevel(LogLevel.Warning); +#endif + }); + host.ConfigureServices((context, services) => { services.AddSingleton(); @@ -53,6 +63,7 @@ static void Main(string[] args) host.UseCommandHandler(); host.UseCommandHandler(); host.UseCommandHandler(); + host.UseCommandHandler(); host.UseCommandHandler(); }) diff --git a/src/PwSafeClient.CLI/README.md b/src/PwSafeClient.CLI/README.md index bf62851..b311907 100644 --- a/src/PwSafeClient.CLI/README.md +++ b/src/PwSafeClient.CLI/README.md @@ -37,6 +37,7 @@ Commands: renew Renew the password of an entry update Update the properties of an entry rm Remove an entry or group from the database + policy Manage your password policies unlock Unlock a database ``` @@ -162,6 +163,51 @@ $ pwsafe rm $ pwsafe rm --group ``` +### Manage the password policy + +#### 1. List existing password policies + +```bash +$ pwsafe policy list +``` + +#### 2. Add new password policy + +```bash +$ pwsafe policy add --name "Sample" \ + --length 12 \ + --uppercase 2 \ + --lowercase 2 \ + --digits 1 \ + --symbols 1 \ + --symbol-chars "@#$%&" \ + --easy-vision +``` + +#### 3. Update an existing password policy + +```bash +$ pwsafe policy update --name "Sample" \ + --length 12 \ + --uppercase 2 \ + --lowercase 2 \ + --digits 1 \ + --symbols=-1 \ + --easy-vision +``` + +#### 4. Remove a password policy + +```bash +$ pwsafe policy rm --name "Sample" +``` + +#### 5. Generate password for given policy + +```bash +$ pwsafe policy genpass --name "Sample" +``` + ### Interactive mode It's boring to enter password for every operation, if you want to do a lot operations, you can unlock the database in interactively mode. The session will automatically exit if you don't take any actions. diff --git a/src/PwSafeClient.CLI/Services/ConfigManager.cs b/src/PwSafeClient.CLI/Services/ConfigManager.cs index 4345b4f..f576f90 100644 --- a/src/PwSafeClient.CLI/Services/ConfigManager.cs +++ b/src/PwSafeClient.CLI/Services/ConfigManager.cs @@ -35,7 +35,7 @@ public ConfigManager(IEnvironmentManager environmentManager, IConsoleService con { this.consoleService = consoleService; - string? homeDirectory = environmentManager.GetHomeDirectory(); + var homeDirectory = environmentManager.GetHomeDirectory(); if (string.IsNullOrEmpty(homeDirectory)) { throw new Exception("Cannot find home directory."); @@ -63,7 +63,7 @@ public async Task AddDatabaseAsync(string alias, string filepath, bool isDefault ArgumentValidator.ThrowIfNullOrWhiteSpace(nameof(alias), alias); ArgumentValidator.ThrowIfNullOrWhiteSpace(nameof(filepath), filepath); - Config config = await LoadConfigAsync(); + var config = await LoadConfigAsync(); config.Databases[alias] = filepath; if (isDefault) @@ -77,7 +77,7 @@ public async Task AddDatabaseAsync(string alias, string filepath, bool isDefault /// public async Task GetDbPathAsync(string? alias) { - Config config = await LoadConfigAsync(); + var config = await LoadConfigAsync(); alias ??= config.DefaultDatabase; if (string.IsNullOrEmpty(alias)) @@ -85,7 +85,7 @@ public async Task GetDbPathAsync(string? alias) throw new Exception("The default database is not configured."); } - if (config.Databases.TryGetValue(alias, out string? filePath)) + if (config.Databases.TryGetValue(alias, out var filePath)) { if (string.IsNullOrEmpty(filePath)) { @@ -105,14 +105,14 @@ public async Task GetDbPathAsync(string? alias) /// public async Task GetIdleTimeAsync() { - Config config = await LoadConfigAsync(); + var config = await LoadConfigAsync(); return config.IdleTime; } /// public async Task GetMaxBackupCountAsync() { - Config config = await LoadConfigAsync(); + var config = await LoadConfigAsync(); return config.MaxBackupCount; } @@ -121,11 +121,11 @@ public async Task LoadConfigAsync() { try { - using FileStream fileStream = File.Open(configFileAbsolutePath, FileMode.Open); - using StreamReader reader = new StreamReader(fileStream); - string json = await reader.ReadToEndAsync(); + using var fileStream = File.Open(configFileAbsolutePath, FileMode.Open); + using var reader = new StreamReader(fileStream); + var json = await reader.ReadToEndAsync(); - Config? config = JsonSerializer.Deserialize(json, options); + var config = JsonSerializer.Deserialize(json, options); return config ?? new Config(); } catch (FileNotFoundException) @@ -155,7 +155,7 @@ public async Task RemoveDatabaseAsync(string alias) { ArgumentValidator.ThrowIfNullOrWhiteSpace(nameof(alias), alias); - Config config = await LoadConfigAsync(); + var config = await LoadConfigAsync(); config.Databases.Remove(alias); if (config.DefaultDatabase == alias) @@ -176,7 +176,7 @@ public Task SaveAsync(Config config) Directory.CreateDirectory(configFolderAbsolutePath); } - string content = JsonSerializer.Serialize(config, options); + var content = JsonSerializer.Serialize(config, options); return File.WriteAllTextAsync(configFileAbsolutePath, content); } catch (Exception e) @@ -191,7 +191,7 @@ public async Task SetDefaultDatabaseAsync(string alias) { ArgumentValidator.ThrowIfNullOrWhiteSpace(nameof(alias), alias); - Config config = await LoadConfigAsync(); + var config = await LoadConfigAsync(); foreach (var item in config.Databases) { diff --git a/src/PwSafeClient.CLI/Services/ConsoleService.cs b/src/PwSafeClient.CLI/Services/ConsoleService.cs index 8821fec..be81451 100644 --- a/src/PwSafeClient.CLI/Services/ConsoleService.cs +++ b/src/PwSafeClient.CLI/Services/ConsoleService.cs @@ -18,7 +18,7 @@ public string ReadPassword() Console.Write("Enter your password: "); while (true) { - ConsoleKeyInfo i = Console.ReadKey(true); + var i = Console.ReadKey(true); if (i.Key == ConsoleKey.Enter) { break; @@ -60,7 +60,7 @@ public bool DoConfirm(string message) public string ReadLine(string symbol = ">") { Console.Write($"{symbol} "); - string? input = Console.ReadLine(); + var input = Console.ReadLine(); return input ?? string.Empty; } diff --git a/src/PwSafeClient.CLI/appsettings.json b/src/PwSafeClient.CLI/appsettings.json deleted file mode 100644 index 55d8d76..0000000 --- a/src/PwSafeClient.CLI/appsettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/appsettings.json", - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.Hosting": "Warning" - } - } -} \ No newline at end of file diff --git a/src/PwSafeClient.Shared/Group.cs b/src/PwSafeClient.Shared/Group.cs index 88830ef..2ce3e17 100644 --- a/src/PwSafeClient.Shared/Group.cs +++ b/src/PwSafeClient.Shared/Group.cs @@ -55,7 +55,7 @@ public GroupPath GetGroupPath() } List segments = [Name]; - Group node = this; + var node = this; while (node.Parent != null) { diff --git a/src/PwSafeClient.Shared/GroupBuilder.cs b/src/PwSafeClient.Shared/GroupBuilder.cs index 0190c74..a2f0e7e 100644 --- a/src/PwSafeClient.Shared/GroupBuilder.cs +++ b/src/PwSafeClient.Shared/GroupBuilder.cs @@ -37,7 +37,7 @@ public Group Build() { var groupList = new List(); - foreach (Entry entry in entries) + foreach (var entry in entries) { if (!string.IsNullOrEmpty(entry.Group) && !groupList.Contains(entry.Group)) { @@ -47,7 +47,7 @@ public Group Build() var orderedGroupList = groupList.OrderBy(item => item.ToString()); - foreach (GroupPath path in orderedGroupList) + foreach (var path in orderedGroupList) { Root.InsertByGroupPath(path); } diff --git a/src/PwSafeClient.Shared/PasswordGenerator.cs b/src/PwSafeClient.Shared/PasswordGenerator.cs index e10219b..414bba4 100644 --- a/src/PwSafeClient.Shared/PasswordGenerator.cs +++ b/src/PwSafeClient.Shared/PasswordGenerator.cs @@ -1,5 +1,5 @@ using System; -using System.Linq; +using System.Collections.Generic; using Medo.Security.Cryptography.PasswordSafe; @@ -24,38 +24,144 @@ public PasswordGenerator(PasswordPolicy passwordPolicy) /// public string GeneratePassword() { - Console.WriteLine($"Generating password..."); - char[] password = new char[passwordPolicy.TotalPasswordLength]; - - bool useEasyVision = passwordPolicy.Style.HasFlag(PasswordPolicyStyle.UseEasyVision); - bool useHexDigits = passwordPolicy.Style.HasFlag(PasswordPolicyStyle.UseHexDigits); - bool useDigits = passwordPolicy.Style.HasFlag(PasswordPolicyStyle.UseDigits); - bool useSymbols = passwordPolicy.Style.HasFlag(PasswordPolicyStyle.UseSymbols); - bool useUppercase = passwordPolicy.Style.HasFlag(PasswordPolicyStyle.UseUppercase); - bool useLowercase = passwordPolicy.Style.HasFlag(PasswordPolicyStyle.UseLowercase); - bool makePronounceable = passwordPolicy.Style.HasFlag(PasswordPolicyStyle.MakePronounceable); + var useHexDigits = passwordPolicy.Style.HasFlag(PasswordPolicyStyle.UseHexDigits); + var makePronounceable = passwordPolicy.Style.HasFlag(PasswordPolicyStyle.MakePronounceable); if (useHexDigits) { - for (var i = 0; i < passwordPolicy.TotalPasswordLength; i++) + return GenerateHexDigitsOnlyPassword(); + } + + if (makePronounceable) + { + return GeneratePronounceablePassword(); + } + + return GenerateClassicPassword(); + } + + private string GenerateHexDigitsOnlyPassword() + { + var password = new char[passwordPolicy.TotalPasswordLength]; + + for (var i = 0; i < passwordPolicy.TotalPasswordLength; i++) + { + password[i] = GetRandomChar(PwCharPool.StdHexDigitChars); + } + + return string.Join("", password); + } + + private string GeneratePronounceablePassword() + { + var password = new char[passwordPolicy.TotalPasswordLength]; + + var useDigits = passwordPolicy.Style.HasFlag(PasswordPolicyStyle.UseDigits); + var useSymbols = passwordPolicy.Style.HasFlag(PasswordPolicyStyle.UseSymbols); + var useUppercase = passwordPolicy.Style.HasFlag(PasswordPolicyStyle.UseUppercase); + var useLowercase = passwordPolicy.Style.HasFlag(PasswordPolicyStyle.UseLowercase); + + // If we don't have any character types, we can't generate a password. + if (!useDigits && !useSymbols && !useUppercase && !useLowercase) + { + return string.Empty; + } + + var index = 0; + var useVowel = false; + var indicators = new List(); + + if (useUppercase) + { + indicators.AddRange("uuuuuu"); + } + + if (useLowercase) + { + indicators.AddRange("llllll"); + } + + if (useDigits) + { + indicators.AddRange("dd"); + } + + if (useSymbols) + { + indicators.AddRange("ss"); + } + + var indicatorsArray = indicators.ToArray(); + + while (index < passwordPolicy.TotalPasswordLength) + { + var indicator = GetRandomChar(indicatorsArray); + + if (indicator == 'u') + { + password[index] = GetRandomChar(useVowel ? PwCharPool.UppercaseVowels : PwCharPool.UppercaseConsonants); + useVowel = !useVowel; + } + + if (indicator == 'l') + { + password[index] = GetRandomChar(useVowel ? PwCharPool.LowercaseVowels : PwCharPool.LowercaseConsonants); + useVowel = !useVowel; + } + + if (indicator == 'd') { - password[i] = GetRandomChar(PwCharPool.StdHexDigitChars); + password[index] = GetRandomChar(PwCharPool.StdDigitChars); + useVowel = false; // Treat digits as vowels } - return string.Join("", password); + if (indicator == 's') + { + password[index] = GetRandomChar(PwCharPool.PronounceableSymbolChars); + useVowel = false; // Treat symbols as vowels + } + + index++; } - if (makePronounceable) + return string.Join("", password); + } + + private string GenerateClassicPassword() + { + var password = new char[passwordPolicy.TotalPasswordLength]; + + var useEasyVision = passwordPolicy.Style.HasFlag(PasswordPolicyStyle.UseEasyVision); + var useDigits = passwordPolicy.Style.HasFlag(PasswordPolicyStyle.UseDigits); + var useSymbols = passwordPolicy.Style.HasFlag(PasswordPolicyStyle.UseSymbols); + var useUppercase = passwordPolicy.Style.HasFlag(PasswordPolicyStyle.UseUppercase); + var useLowercase = passwordPolicy.Style.HasFlag(PasswordPolicyStyle.UseLowercase); + + var uppercaseChars = useEasyVision ? PwCharPool.EasyVisionUppercaseChars : PwCharPool.StdUppercaseChars; + var lowercaseChars = useEasyVision ? PwCharPool.EasyVisionLowercaseChars : PwCharPool.StdLowercaseChars; + var digitChars = useEasyVision ? PwCharPool.EasyVisionDigitChars : PwCharPool.StdDigitChars; + var symbolChars = passwordPolicy.GetSpecialSymbolSet(); + + if (symbolChars.Length == 0) + { + symbolChars = useEasyVision ? PwCharPool.EasyVisionSymbolChars : PwCharPool.StdSymbolChars; + } + + var allChars = new List(); + + // If we don't have any character types, we can't generate a password. + if (!useDigits && !useSymbols && !useUppercase && !useLowercase) { - return MakePronounceablePassword(); + return string.Empty; } - char[] uppercaseChars = useEasyVision ? PwCharPool.EasyVisionUppercase_chars : PwCharPool.StdUppercaseChars; - char[] lowercaseChars = useEasyVision ? PwCharPool.EasyVisionLowercaseChars : PwCharPool.StdLowercaseChars; - char[] digitChars = useEasyVision ? PwCharPool.EasyVisionDigitChars : PwCharPool.StdDigitChars; - char[] symbolChars = useEasyVision ? PwCharPool.EasyVisionSymbolChars : PwCharPool.StdSymbolChars; + // If the sum of the minimum counts is greater than the total length, then we can't generate a password. + if (passwordPolicy.MinimumSymbolCount + passwordPolicy.MinimumDigitCount + passwordPolicy.MinimumLowercaseCount + passwordPolicy.MinimumUppercaseCount > passwordPolicy.TotalPasswordLength) + { + return string.Empty; + } - int n = 0; + var n = 0; if (useUppercase) { @@ -64,6 +170,8 @@ public string GeneratePassword() password[n + i] = GetRandomChar(uppercaseChars); n++; } + + allChars.AddRange(uppercaseChars); } if (useLowercase) @@ -73,6 +181,8 @@ public string GeneratePassword() password[n + i] = GetRandomChar(lowercaseChars); n++; } + + allChars.AddRange(lowercaseChars); } if (useDigits) @@ -82,6 +192,8 @@ public string GeneratePassword() password[n + i] = GetRandomChar(digitChars); n++; } + + allChars.AddRange(digitChars); } if (useSymbols) @@ -91,32 +203,31 @@ public string GeneratePassword() password[n + i] = GetRandomChar(symbolChars); n++; } + + allChars.AddRange(symbolChars); } + var allCharsArray = allChars.ToArray(); + for (var i = n; i < passwordPolicy.TotalPasswordLength; i++) { - password[i] = GetRandomChar(uppercaseChars.Concat(lowercaseChars).Concat(digitChars).Concat(symbolChars).ToArray()); + password[i] = GetRandomChar(allCharsArray); } password = Shuffle(password); return string.Join("", password); } - /// - /// TODO: Generate a pronounceable password. - /// - /// Pronounceable password - private string MakePronounceablePassword() + private static char GetRandomChar(char[] chars) { - char[] password = new char[passwordPolicy.TotalPasswordLength]; - return string.Join("", password); + var randomChar = chars[GetRandomInt(chars.Length)]; + return randomChar; } - private static char GetRandomChar(char[] chars) + private static int GetRandomInt(int max) { var random = new Random(); - var randomChar = chars[random.Next(chars.Length)]; - return randomChar; + return random.Next(max); } private static char[] Shuffle(char[] password) @@ -126,8 +237,8 @@ private static char[] Shuffle(char[] password) while (count > 0) { - var index1 = new Random().Next(length); - var index2 = new Random().Next(length); + var index1 = GetRandomInt(length); + var index2 = GetRandomInt(length); if (index1 == index2) { diff --git a/src/PwSafeClient.Shared/PwCharPool.cs b/src/PwSafeClient.Shared/PwCharPool.cs index eecf3d8..5863d89 100644 --- a/src/PwSafeClient.Shared/PwCharPool.cs +++ b/src/PwSafeClient.Shared/PwCharPool.cs @@ -10,6 +10,14 @@ public static class PwCharPool public static readonly char[] StdUppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".ToCharArray(); + public static readonly char[] LowercaseVowels = "aeiouy".ToCharArray(); + + public static readonly char[] UppercaseVowels = "AEIOUY".ToCharArray(); + + public static readonly char[] LowercaseConsonants = "bcdfghjklmnpqrstvwxz".ToCharArray(); + + public static readonly char[] UppercaseConsonants = "BCDFGHJKLMNPQRSTVWXZ".ToCharArray(); + public static readonly char[] StdDigitChars = "0123456789".ToCharArray(); public static readonly char[] StdHexDigitChars = "0123456789abcdef".ToCharArray(); @@ -18,7 +26,7 @@ public static class PwCharPool public static readonly char[] EasyVisionLowercaseChars = "abcdefghijkmnopqrstuvwxyz".ToCharArray(); - public static readonly char[] EasyVisionUppercase_chars = "ABCDEFGHJKLMNPQRTUVWXY".ToCharArray(); + public static readonly char[] EasyVisionUppercaseChars = "ABCDEFGHJKLMNPQRTUVWXY".ToCharArray(); public static readonly char[] EasyVisionDigitChars = "346789".ToCharArray(); diff --git a/test/PwSafeClient.Shared.UnitTests/GroupBuilderTests.cs b/test/PwSafeClient.Shared.UnitTests/GroupBuilderTests.cs index 3af574d..53d8ef9 100644 --- a/test/PwSafeClient.Shared.UnitTests/GroupBuilderTests.cs +++ b/test/PwSafeClient.Shared.UnitTests/GroupBuilderTests.cs @@ -34,12 +34,12 @@ public void BuildGroupTest() Assert.AreEqual("group1", group.Children[0].Name); Assert.AreEqual("group2", group.Children[1].Name); - Group group1 = group.Children[0]; + var group1 = group.Children[0]; Assert.AreEqual(2, group1.Children.Count); Assert.AreEqual("group1-1", group1.Children[0].Name); Assert.AreEqual("group1-2", group1.Children[1].Name); - Group group2 = group.Children[1]; + var group2 = group.Children[1]; Assert.AreEqual(1, group2.Children.Count); } } diff --git a/test/PwSafeClient.Shared.UnitTests/GroupTests.cs b/test/PwSafeClient.Shared.UnitTests/GroupTests.cs index 162c5f6..98f2ccb 100644 --- a/test/PwSafeClient.Shared.UnitTests/GroupTests.cs +++ b/test/PwSafeClient.Shared.UnitTests/GroupTests.cs @@ -54,7 +54,7 @@ public void InsertBySegmentsTest() root.InsertBySegments(["group1", "group4"]); - Group group1 = root.Children[0]; + var group1 = root.Children[0]; Assert.IsNotNull(group1); Assert.AreEqual(2, group1.Children.Count);