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);