diff --git a/Cyberduck.sln b/Cyberduck.sln index 07d25e7a0d..432b0e0c6a 100644 --- a/Cyberduck.sln +++ b/Cyberduck.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 -VisualStudioVersion = 18.2.11415.280 d18.0 +VisualStudioVersion = 18.2.11415.280 MinimumVisualStudioVersion = 15.0.26124.0 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{8B08EF96-10D6-4F35-94C1-986F9F0F1506}" EndProject @@ -61,6 +61,8 @@ Project("{DAEA77DE-8320-43BA-BA7C-EF5C12478AB5}") = "Cyberduck.Cryptomator", "cr EndProject Project("{DAEA77DE-8320-43BA-BA7C-EF5C12478AB5}") = "Cyberduck.Cli", "cli\dll\Cyberduck.Cli.ikvmproj", "{2D33598A-21A1-4117-82DC-250F4CE8D5E5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "test-utils", "windows\src\test\utils\test-utils.csproj", "{BBCC0F0C-0135-AAAB-DB32-CC4E28BC88E0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -141,6 +143,10 @@ Global {2D33598A-21A1-4117-82DC-250F4CE8D5E5}.Debug|x64.Build.0 = Debug|Any CPU {2D33598A-21A1-4117-82DC-250F4CE8D5E5}.Release|x64.ActiveCfg = Release|Any CPU {2D33598A-21A1-4117-82DC-250F4CE8D5E5}.Release|x64.Build.0 = Release|Any CPU + {BBCC0F0C-0135-AAAB-DB32-CC4E28BC88E0}.Debug|x64.ActiveCfg = Debug|Any CPU + {BBCC0F0C-0135-AAAB-DB32-CC4E28BC88E0}.Debug|x64.Build.0 = Debug|Any CPU + {BBCC0F0C-0135-AAAB-DB32-CC4E28BC88E0}.Release|x64.ActiveCfg = Release|Any CPU + {BBCC0F0C-0135-AAAB-DB32-CC4E28BC88E0}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -168,6 +174,7 @@ Global {9C7B827F-AE30-44C4-A210-E49DF883C720} = {72B4BA09-65D8-4C49-930E-B14104B2AB1B} {7EFC0398-8F4D-4850-BBE3-A0CC85410559} = {72B4BA09-65D8-4C49-930E-B14104B2AB1B} {2D33598A-21A1-4117-82DC-250F4CE8D5E5} = {72B4BA09-65D8-4C49-930E-B14104B2AB1B} + {BBCC0F0C-0135-AAAB-DB32-CC4E28BC88E0} = {8B08EF96-10D6-4F35-94C1-986F9F0F1506} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {289E6003-15D5-4377-ADA6-2E7093785BCD} diff --git a/Directory.Packages.props b/Directory.Packages.props index a53498443a..d16e1d5ab6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -39,6 +39,7 @@ + diff --git a/windows/src/test/utils/CredDeleteCommand.cs b/windows/src/test/utils/CredDeleteCommand.cs new file mode 100644 index 0000000000..ed61ccfefe --- /dev/null +++ b/windows/src/test/utils/CredDeleteCommand.cs @@ -0,0 +1,62 @@ +using System; +using System.CommandLine; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using Windows.Win32; +using Windows.Win32.Security.Credentials; +using static Program; + +namespace test_utils; + +internal static class CredDeleteCommand +{ + internal static unsafe void Invoke(ParseResult result) + { + var patternValues = result.GetValue(Program.CredMatch); + Regex[] patterns = null; + if (patternValues is not null) + { + patterns = new Regex[patternValues.Length]; + var patternWriter = ((Span)patterns).GetEnumerator(); + foreach (var item in patternValues) + { + patternWriter.MoveNext(); + patternWriter.Current = new Regex(item, RegexOptions.Compiled); + } + } + + CREDENTIALW** credentials = null; + try + { + if (!PInvoke.CredEnumerate(null, out var count, out credentials)) + { + throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()); + } + + foreach (ref readonly var credential in new ReadOnlySpan(credentials, (int)count)) + { + bool? matched; + if ((matched = patterns?.Any(credential.Value.TargetName.ToString().Match)) is false) + { + continue; + } + + Console.WriteLine($"{credential.Value.TargetName} ({credential.Value.UserName})"); + if (matched is true) + { + if (!PInvoke.CredDelete(credential.Value.TargetName, credential.Value.Type, 0)) + { + Console.WriteLine($" Failure deleting: {Marshal.GetLastPInvokeErrorMessage()}"); + } + } + } + } + finally + { + PInvoke.CredFree(credentials); + } + } + + private static bool Match(this string text, Regex pattern) => pattern.IsMatch(text); +} diff --git a/windows/src/test/utils/NativeMethods.txt b/windows/src/test/utils/NativeMethods.txt new file mode 100644 index 0000000000..557f60df4f --- /dev/null +++ b/windows/src/test/utils/NativeMethods.txt @@ -0,0 +1,3 @@ +CredDelete +CredEnumerate +CredFree diff --git a/windows/src/test/utils/Program.cs b/windows/src/test/utils/Program.cs new file mode 100644 index 0000000000..d41b304302 --- /dev/null +++ b/windows/src/test/utils/Program.cs @@ -0,0 +1,30 @@ +using System.CommandLine; +using test_utils; +using Windows.Win32.Security.Credentials; + +RootCommand command = []; + +Command credDeleteCommand = new("cred-delete", "Lists Credentials, and optionally deletes them when matching."); +command.Add(credDeleteCommand); +credDeleteCommand.SetAction(CredDeleteCommand.Invoke); +credDeleteCommand.Add(CredMatch = new("--match") +{ + AllowMultipleArgumentsPerToken = true, + Arity = ArgumentArity.ZeroOrMore, + Description = "When specified, Cred-Delete will delete items matching any of the Regex patterns." +}); + + +return command.Parse(args).Invoke(); + +partial class Program +{ + internal static Option CredMatch; + + internal readonly unsafe struct PCREDENTIALW + { + private readonly CREDENTIALW* _ptr; + + public readonly ref CREDENTIALW Value => ref *_ptr; + } +} diff --git a/windows/src/test/utils/test-utils.csproj b/windows/src/test/utils/test-utils.csproj new file mode 100644 index 0000000000..c9c7de15e2 --- /dev/null +++ b/windows/src/test/utils/test-utils.csproj @@ -0,0 +1,13 @@ + + + + Exe + net8.0-win + + + + + + + + \ No newline at end of file