From 0daadeb0e879c800920ae626c8ad21bd798f8feb Mon Sep 17 00:00:00 2001 From: Yoshi Askharoun Date: Thu, 23 Jun 2022 20:49:33 -0500 Subject: [PATCH] Added Meziantou.Framework.Win32.CredentialManager fork --- ZuneShell.sln | 27 ++- .../Credential.cs | 25 +++ .../CredentialManager.cs | 177 ++++++++++++++++++ .../CredentialPersistence.cs | 9 + .../CredentialType.cs | 14 ++ .../CriticalCredentialHandle.cs | 36 ++++ .../CriticalHandleZeroOrMinusOneIsInvalid.cs | 17 ++ ...u.Framework.Win32.CredentialManager.csproj | 11 ++ .../Natives/Advapi32.cs | 23 +++ .../Natives/Credential.cs | 22 +++ 10 files changed, 357 insertions(+), 4 deletions(-) create mode 100644 libs/Meziantou.Framework.Win32.CredentialManager/Credential.cs create mode 100644 libs/Meziantou.Framework.Win32.CredentialManager/CredentialManager.cs create mode 100644 libs/Meziantou.Framework.Win32.CredentialManager/CredentialPersistence.cs create mode 100644 libs/Meziantou.Framework.Win32.CredentialManager/CredentialType.cs create mode 100644 libs/Meziantou.Framework.Win32.CredentialManager/CriticalCredentialHandle.cs create mode 100644 libs/Meziantou.Framework.Win32.CredentialManager/CriticalHandleZeroOrMinusOneIsInvalid.cs create mode 100644 libs/Meziantou.Framework.Win32.CredentialManager/Meziantou.Framework.Win32.CredentialManager.csproj create mode 100644 libs/Meziantou.Framework.Win32.CredentialManager/Natives/Advapi32.cs create mode 100644 libs/Meziantou.Framework.Win32.CredentialManager/Natives/Credential.cs diff --git a/ZuneShell.sln b/ZuneShell.sln index 94a3168..be3ed2a 100644 --- a/ZuneShell.sln +++ b/ZuneShell.sln @@ -18,6 +18,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UIX.Debug", "libs\Microsoft EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZuneImpl", "ZuneImpl\ZuneImpl.csproj", "{0B309776-6424-48C4-838A-224E41F793EE}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meziantou.Framework.Win32.CredentialManager", "libs\Meziantou.Framework.Win32.CredentialManager\Meziantou.Framework.Win32.CredentialManager.csproj", "{90A89707-927E-48D2-BFEB-84F95F531258}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -126,10 +128,10 @@ Global {8479F68C-4234-425E-93E7-71597AF528E9}.Release|x64.Build.0 = Release|Any CPU {8479F68C-4234-425E-93E7-71597AF528E9}.Release|x86.ActiveCfg = Release|Any CPU {8479F68C-4234-425E-93E7-71597AF528E9}.Release|x86.Build.0 = Release|Any CPU - {0B309776-6424-48C4-838A-224E41F793EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0B309776-6424-48C4-838A-224E41F793EE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0B309776-6424-48C4-838A-224E41F793EE}.Debug|ARM32.ActiveCfg = Debug|Any CPU - {0B309776-6424-48C4-838A-224E41F793EE}.Debug|ARM32.Build.0 = Debug|Any CPU + {0B309776-6424-48C4-838A-224E41F793EE}.Debug|Any CPU.ActiveCfg = Debug|x64 + {0B309776-6424-48C4-838A-224E41F793EE}.Debug|Any CPU.Build.0 = Debug|x64 + {0B309776-6424-48C4-838A-224E41F793EE}.Debug|ARM32.ActiveCfg = Debug|x64 + {0B309776-6424-48C4-838A-224E41F793EE}.Debug|ARM32.Build.0 = Debug|x64 {0B309776-6424-48C4-838A-224E41F793EE}.Debug|x64.ActiveCfg = Debug|x64 {0B309776-6424-48C4-838A-224E41F793EE}.Debug|x64.Build.0 = Debug|x64 {0B309776-6424-48C4-838A-224E41F793EE}.Debug|x86.ActiveCfg = Debug|x86 @@ -142,6 +144,22 @@ Global {0B309776-6424-48C4-838A-224E41F793EE}.Release|x64.Build.0 = Release|x64 {0B309776-6424-48C4-838A-224E41F793EE}.Release|x86.ActiveCfg = Release|Any CPU {0B309776-6424-48C4-838A-224E41F793EE}.Release|x86.Build.0 = Release|Any CPU + {90A89707-927E-48D2-BFEB-84F95F531258}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90A89707-927E-48D2-BFEB-84F95F531258}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90A89707-927E-48D2-BFEB-84F95F531258}.Debug|ARM32.ActiveCfg = Debug|Any CPU + {90A89707-927E-48D2-BFEB-84F95F531258}.Debug|ARM32.Build.0 = Debug|Any CPU + {90A89707-927E-48D2-BFEB-84F95F531258}.Debug|x64.ActiveCfg = Debug|x64 + {90A89707-927E-48D2-BFEB-84F95F531258}.Debug|x64.Build.0 = Debug|x64 + {90A89707-927E-48D2-BFEB-84F95F531258}.Debug|x86.ActiveCfg = Debug|x86 + {90A89707-927E-48D2-BFEB-84F95F531258}.Debug|x86.Build.0 = Debug|x86 + {90A89707-927E-48D2-BFEB-84F95F531258}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90A89707-927E-48D2-BFEB-84F95F531258}.Release|Any CPU.Build.0 = Release|Any CPU + {90A89707-927E-48D2-BFEB-84F95F531258}.Release|ARM32.ActiveCfg = Release|Any CPU + {90A89707-927E-48D2-BFEB-84F95F531258}.Release|ARM32.Build.0 = Release|Any CPU + {90A89707-927E-48D2-BFEB-84F95F531258}.Release|x64.ActiveCfg = Release|Any CPU + {90A89707-927E-48D2-BFEB-84F95F531258}.Release|x64.Build.0 = Release|Any CPU + {90A89707-927E-48D2-BFEB-84F95F531258}.Release|x86.ActiveCfg = Release|Any CPU + {90A89707-927E-48D2-BFEB-84F95F531258}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -150,6 +168,7 @@ Global {78A8B08C-C0E9-4BAC-9AF6-58D3A55C05C9} = {7640D6FE-600E-44BE-A59F-E90602FA7754} {CB2B7609-C7B8-4E2A-82D8-DD631B70B621} = {7640D6FE-600E-44BE-A59F-E90602FA7754} {8479F68C-4234-425E-93E7-71597AF528E9} = {7640D6FE-600E-44BE-A59F-E90602FA7754} + {90A89707-927E-48D2-BFEB-84F95F531258} = {7640D6FE-600E-44BE-A59F-E90602FA7754} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6B948258-B457-4B3D-955E-05ED3F03CF36} diff --git a/libs/Meziantou.Framework.Win32.CredentialManager/Credential.cs b/libs/Meziantou.Framework.Win32.CredentialManager/Credential.cs new file mode 100644 index 0000000..9791d06 --- /dev/null +++ b/libs/Meziantou.Framework.Win32.CredentialManager/Credential.cs @@ -0,0 +1,25 @@ +namespace Meziantou.Framework.Win32 +{ + public class Credential + { + public CredentialType CredentialType { get; } + public string ApplicationName { get; } + public string UserName { get; } + public string Password { get; } + public string Comment { get; } + + public Credential(CredentialType credentialType, string applicationName, string userName, string password, string comment) + { + ApplicationName = applicationName; + UserName = userName; + Password = password; + CredentialType = credentialType; + Comment = comment; + } + + public override string ToString() + { + return $"CredentialType: {CredentialType}, ApplicationName: {ApplicationName}, UserName: {UserName}, Password: {Password}, Comment: {Comment}"; + } + } +} \ No newline at end of file diff --git a/libs/Meziantou.Framework.Win32.CredentialManager/CredentialManager.cs b/libs/Meziantou.Framework.Win32.CredentialManager/CredentialManager.cs new file mode 100644 index 0000000..6e9f8cd --- /dev/null +++ b/libs/Meziantou.Framework.Win32.CredentialManager/CredentialManager.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Text; +using Meziantou.Framework.Win32.Natives; + +namespace Meziantou.Framework.Win32 +{ + public static class CredentialManager + { + public static Credential ReadCredential(string applicationName) + { + var read = Advapi32.CredRead(applicationName, CredentialType.Generic, 0, out var nCredPtr); + if (read) + { + using (var critCred = new CriticalCredentialHandle(nCredPtr)) + { + var cred = critCred.GetCredential(); + return ReadCredential(cred); + } + } + + return null; + } + + private static Credential ReadCredential(CREDENTIAL credential) + { + var applicationName = Marshal.PtrToStringUni(credential.TargetName); + var userName = Marshal.PtrToStringUni(credential.UserName); + string secret = null; + if (credential.CredentialBlob != IntPtr.Zero) + { + secret = Marshal.PtrToStringUni(credential.CredentialBlob, (int)credential.CredentialBlobSize / 2); + } + + string comment = null; + if (credential.Comment != IntPtr.Zero) + { + comment = Marshal.PtrToStringUni(credential.Comment); + } + + return new Credential(credential.Type, applicationName, userName, secret, comment); + } + + public static void WriteCredential(string applicationName, string userName, string secret, CredentialPersistence persistence) + { + WriteCredential(applicationName, userName, secret, null, persistence); + } + + public static void WriteCredential(string applicationName, string userName, string secret, string comment, CredentialPersistence persistence) + { + if (applicationName == null) + throw new ArgumentNullException(nameof(applicationName)); + + if (userName == null) + throw new ArgumentNullException(nameof(userName)); + + if (secret == null) + throw new ArgumentNullException(nameof(secret)); + + // CRED_MAX_CREDENTIAL_BLOB_SIZE + // XP and Vista: 512; + // 7 and above: 5*512 + var secretLength = secret.Length * UnicodeEncoding.CharSize; + if (Environment.OSVersion.Version < new Version(6, 1) /* Windows 7 */) + { + if (secretLength > 512) + throw new ArgumentOutOfRangeException(nameof(secret), "The secret message has exceeded 512 bytes."); + } + else + { + if (secretLength > 2560) + throw new ArgumentOutOfRangeException(nameof(secret), "The secret message has exceeded 2560 bytes."); + } + + if (comment != null) + { + // CRED_MAX_STRING_LENGTH 256 + if (comment.Length > 255) + throw new ArgumentOutOfRangeException(nameof(comment), "The comment message has exceeded 256 characters."); + } + + var commentPtr = IntPtr.Zero; + var targetNamePtr = IntPtr.Zero; + var credentialBlobPtr = IntPtr.Zero; + var userNamePtr = IntPtr.Zero; + try + { + commentPtr = comment != null ? Marshal.StringToCoTaskMemUni(comment) : IntPtr.Zero; + targetNamePtr = Marshal.StringToCoTaskMemUni(applicationName); + credentialBlobPtr = Marshal.StringToCoTaskMemUni(secret); + userNamePtr = Marshal.StringToCoTaskMemUni(userName); + + var credential = new CREDENTIAL + { + AttributeCount = 0, + Attributes = IntPtr.Zero, + Comment = commentPtr, + TargetAlias = IntPtr.Zero, + Type = CredentialType.Generic, + Persist = persistence, + CredentialBlobSize = (uint)secretLength, + TargetName = targetNamePtr, + CredentialBlob = credentialBlobPtr, + UserName = userNamePtr + }; + + var written = Advapi32.CredWrite(ref credential, 0); + var lastError = Marshal.GetLastWin32Error(); + if (!written) + throw new Win32Exception(lastError, $"CredWrite failed with the error code {lastError}."); + } + finally + { + FreeCoTaskMem(commentPtr); + FreeCoTaskMem(targetNamePtr); + FreeCoTaskMem(credentialBlobPtr); + FreeCoTaskMem(userNamePtr); + } + } + + public static void DeleteCredential(string applicationName) + { + if (applicationName == null) + throw new ArgumentNullException(nameof(applicationName)); + + var success = Advapi32.CredDelete(applicationName, CredentialType.Generic, 0); + if (!success) + { + var lastError = Marshal.GetLastWin32Error(); + throw new Win32Exception(lastError); + } + } + + public static IReadOnlyList EnumerateCrendentials() + { + return EnumerateCrendentials(null); + } + + public static IReadOnlyList EnumerateCrendentials(string filter) + { + var result = new List(); + var ret = Advapi32.CredEnumerate(filter, 0, out var count, out var pCredentials); + try + { + if (ret) + { + for (var n = 0; n < count; n++) + { + var credential = Marshal.ReadIntPtr(pCredentials, n * Marshal.SizeOf()); + result.Add(ReadCredential(Marshal.PtrToStructure(credential))); + } + } + else + { + var lastError = Marshal.GetLastWin32Error(); + throw new Win32Exception(lastError); + } + } + finally + { + Advapi32.CredFree(pCredentials); + } + + return result; + } + + private static void FreeCoTaskMem(IntPtr ptr) + { + if (ptr == IntPtr.Zero) + return; + + Marshal.FreeCoTaskMem(ptr); + } + } +} \ No newline at end of file diff --git a/libs/Meziantou.Framework.Win32.CredentialManager/CredentialPersistence.cs b/libs/Meziantou.Framework.Win32.CredentialManager/CredentialPersistence.cs new file mode 100644 index 0000000..13580c7 --- /dev/null +++ b/libs/Meziantou.Framework.Win32.CredentialManager/CredentialPersistence.cs @@ -0,0 +1,9 @@ +namespace Meziantou.Framework.Win32 +{ + public enum CredentialPersistence : uint + { + Session = 1, + LocalMachine, + Enterprise + } +} \ No newline at end of file diff --git a/libs/Meziantou.Framework.Win32.CredentialManager/CredentialType.cs b/libs/Meziantou.Framework.Win32.CredentialManager/CredentialType.cs new file mode 100644 index 0000000..52459e2 --- /dev/null +++ b/libs/Meziantou.Framework.Win32.CredentialManager/CredentialType.cs @@ -0,0 +1,14 @@ +namespace Meziantou.Framework.Win32 +{ + public enum CredentialType + { + Generic = 1, + DomainPassword, + DomainCertificate, + DomainVisiblePassword, + GenericCertificate, + DomainExtended, + Maximum, + MaximumEx = Maximum + 1000, + } +} \ No newline at end of file diff --git a/libs/Meziantou.Framework.Win32.CredentialManager/CriticalCredentialHandle.cs b/libs/Meziantou.Framework.Win32.CredentialManager/CriticalCredentialHandle.cs new file mode 100644 index 0000000..3eacfb8 --- /dev/null +++ b/libs/Meziantou.Framework.Win32.CredentialManager/CriticalCredentialHandle.cs @@ -0,0 +1,36 @@ +using System; +using System.Runtime.InteropServices; +using Meziantou.Framework.Win32.Natives; + +namespace Meziantou.Framework.Win32 +{ + internal sealed class CriticalCredentialHandle : CriticalHandleZeroOrMinusOneIsInvalid + { + public CriticalCredentialHandle(IntPtr preexistingHandle) + { + SetHandle(preexistingHandle); + } + + public CREDENTIAL GetCredential() + { + if (!IsInvalid) + { + return Marshal.PtrToStructure(handle); + } + + throw new InvalidOperationException("Invalid CriticalHandle!"); + } + + protected override bool ReleaseHandle() + { + if (!IsInvalid) + { + Advapi32.CredFree(handle); + SetHandleAsInvalid(); + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/libs/Meziantou.Framework.Win32.CredentialManager/CriticalHandleZeroOrMinusOneIsInvalid.cs b/libs/Meziantou.Framework.Win32.CredentialManager/CriticalHandleZeroOrMinusOneIsInvalid.cs new file mode 100644 index 0000000..e2db967 --- /dev/null +++ b/libs/Meziantou.Framework.Win32.CredentialManager/CriticalHandleZeroOrMinusOneIsInvalid.cs @@ -0,0 +1,17 @@ +using System; +using System.Runtime.InteropServices; + +namespace Meziantou.Framework.Win32 +{ + /// + /// Original code: https://github.com/dotnet/corefx/blob/d0dc5fc099946adc1035b34a8b1f6042eddb0c75/src/Common/src/Microsoft/Win32/SafeHandles/CriticalHandleZeroOrMinusOneIsInvalid.cs + /// + internal abstract class CriticalHandleZeroOrMinusOneIsInvalid : CriticalHandle + { + protected CriticalHandleZeroOrMinusOneIsInvalid() : base(IntPtr.Zero) + { + } + + public override bool IsInvalid => handle == new IntPtr(0) || handle == new IntPtr(-1); + } +} diff --git a/libs/Meziantou.Framework.Win32.CredentialManager/Meziantou.Framework.Win32.CredentialManager.csproj b/libs/Meziantou.Framework.Win32.CredentialManager/Meziantou.Framework.Win32.CredentialManager.csproj new file mode 100644 index 0000000..4524bae --- /dev/null +++ b/libs/Meziantou.Framework.Win32.CredentialManager/Meziantou.Framework.Win32.CredentialManager.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0;net461 + C# wrapper around CredWrite/CredRead/CredDelete functions to store and retrieve from Windows Credential Store + Meziantou.Framework.Win32 + 1.1.1 + AnyCPU;x64;x86 + + + diff --git a/libs/Meziantou.Framework.Win32.CredentialManager/Natives/Advapi32.cs b/libs/Meziantou.Framework.Win32.CredentialManager/Natives/Advapi32.cs new file mode 100644 index 0000000..70ec69c --- /dev/null +++ b/libs/Meziantou.Framework.Win32.CredentialManager/Natives/Advapi32.cs @@ -0,0 +1,23 @@ +using System; +using System.Runtime.InteropServices; + +namespace Meziantou.Framework.Win32.Natives +{ + internal static class Advapi32 + { + [DllImport("Advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool CredRead(string target, CredentialType type, int reservedFlag, out IntPtr credentialPtr); + + [DllImport("Advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool CredDelete(string target, CredentialType type, int reservedFlag); + + [DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool CredWrite([In] ref CREDENTIAL userCredential, [In] uint flags); + + [DllImport("advapi32", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CredEnumerate(string filter, int flag, out int count, out IntPtr pCredentials); + + [DllImport("Advapi32.dll", EntryPoint = "CredFree", SetLastError = true)] + public static extern bool CredFree([In] IntPtr cred); + } +} diff --git a/libs/Meziantou.Framework.Win32.CredentialManager/Natives/Credential.cs b/libs/Meziantou.Framework.Win32.CredentialManager/Natives/Credential.cs new file mode 100644 index 0000000..4484eb4 --- /dev/null +++ b/libs/Meziantou.Framework.Win32.CredentialManager/Natives/Credential.cs @@ -0,0 +1,22 @@ +using System; +using System.Runtime.InteropServices; + +namespace Meziantou.Framework.Win32.Natives +{ + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct CREDENTIAL + { + public uint Flags; + public CredentialType Type; + public IntPtr TargetName; + public IntPtr Comment; + public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten; + public uint CredentialBlobSize; + public IntPtr CredentialBlob; + public CredentialPersistence Persist; + public uint AttributeCount; + public IntPtr Attributes; + public IntPtr TargetAlias; + public IntPtr UserName; + } +} \ No newline at end of file