From d7dc0703cf7e15f4a3f5c11d834e4e398d1da23a Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 18 Mar 2019 16:04:54 +0000 Subject: [PATCH] Move all interop code under Interop namespace Move all the platform specific interop code to the `Interop` namespace and also group the P/Invoke calls into classes representing the modules/libraries/headers the original C functions live in. --- .../Commands/EraseCommandTests.cs | 1 - .../Commands/StoreCommandTests.cs | 1 - .../MacOS}/MacOSKeychainTests.cs | 4 +- .../Windows}/WindowsCredentialManagerTests.cs | 4 +- .../Application.cs | 1 - .../CommandContext.cs | 3 +- .../Commands/EraseCommand.cs | 1 - .../Commands/GetCommand.cs | 1 - .../{GitCredential.cs => Credential.cs} | 33 ++-- .../{SecureStorage => }/ICredentialStore.cs | 3 +- .../InteropUtils.cs} | 4 +- .../MacOS}/MacOSKeychain.cs | 17 +- .../Interop/MacOS/Native/CoreFoundation.cs | 15 ++ .../Interop/MacOS/Native/SecurityFramework.cs | 145 +++++++++++++++++ .../Interop/Windows/Native/Advapi32.cs | 72 +++++++++ .../Interop/Windows/Native/Common.cs | 42 +++++ .../Windows}/WindowsCredentialManager.cs | 36 ++--- .../SecureStorage/Credential.cs | 25 --- .../SecureStorage/NativeMethods.Mac.cs | 153 ------------------ .../SecureStorage/NativeMethods.Windows.cs | 103 ------------ .../Objects/TestCommandContext.cs | 1 - .../Objects/TestCredentialStore.cs | 1 - 22 files changed, 334 insertions(+), 332 deletions(-) rename src/shared/Microsoft.Git.CredentialManager.Tests/{SecureStorage => Interop/MacOS}/MacOSKeychainTests.cs (94%) rename src/shared/Microsoft.Git.CredentialManager.Tests/{SecureStorage => Interop/Windows}/WindowsCredentialManagerTests.cs (94%) rename src/shared/Microsoft.Git.CredentialManager/{GitCredential.cs => Credential.cs} (67%) rename src/shared/Microsoft.Git.CredentialManager/{SecureStorage => }/ICredentialStore.cs (95%) rename src/shared/Microsoft.Git.CredentialManager/{SecureStorage/NativeMethods.cs => Interop/InteropUtils.cs} (79%) rename src/shared/Microsoft.Git.CredentialManager/{SecureStorage => Interop/MacOS}/MacOSKeychain.cs (92%) create mode 100644 src/shared/Microsoft.Git.CredentialManager/Interop/MacOS/Native/CoreFoundation.cs create mode 100644 src/shared/Microsoft.Git.CredentialManager/Interop/MacOS/Native/SecurityFramework.cs create mode 100644 src/shared/Microsoft.Git.CredentialManager/Interop/Windows/Native/Advapi32.cs create mode 100644 src/shared/Microsoft.Git.CredentialManager/Interop/Windows/Native/Common.cs rename src/shared/Microsoft.Git.CredentialManager/{SecureStorage => Interop/Windows}/WindowsCredentialManager.cs (71%) delete mode 100644 src/shared/Microsoft.Git.CredentialManager/SecureStorage/Credential.cs delete mode 100644 src/shared/Microsoft.Git.CredentialManager/SecureStorage/NativeMethods.Mac.cs delete mode 100644 src/shared/Microsoft.Git.CredentialManager/SecureStorage/NativeMethods.Windows.cs diff --git a/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/EraseCommandTests.cs b/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/EraseCommandTests.cs index 42a92e59e..aff4b57b3 100644 --- a/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/EraseCommandTests.cs +++ b/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/EraseCommandTests.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. using System.Threading.Tasks; using Microsoft.Git.CredentialManager.Commands; -using Microsoft.Git.CredentialManager.SecureStorage; using Microsoft.Git.CredentialManager.Tests.Objects; using Moq; using Xunit; diff --git a/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/StoreCommandTests.cs b/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/StoreCommandTests.cs index 0d24ad098..0a3a0af91 100644 --- a/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/StoreCommandTests.cs +++ b/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/StoreCommandTests.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. using System.Threading.Tasks; using Microsoft.Git.CredentialManager.Commands; -using Microsoft.Git.CredentialManager.SecureStorage; using Microsoft.Git.CredentialManager.Tests.Objects; using Moq; using Xunit; diff --git a/src/shared/Microsoft.Git.CredentialManager.Tests/SecureStorage/MacOSKeychainTests.cs b/src/shared/Microsoft.Git.CredentialManager.Tests/Interop/MacOS/MacOSKeychainTests.cs similarity index 94% rename from src/shared/Microsoft.Git.CredentialManager.Tests/SecureStorage/MacOSKeychainTests.cs rename to src/shared/Microsoft.Git.CredentialManager.Tests/Interop/MacOS/MacOSKeychainTests.cs index 2d496ae17..2a253b177 100644 --- a/src/shared/Microsoft.Git.CredentialManager.Tests/SecureStorage/MacOSKeychainTests.cs +++ b/src/shared/Microsoft.Git.CredentialManager.Tests/Interop/MacOS/MacOSKeychainTests.cs @@ -2,9 +2,9 @@ // Licensed under the MIT license. using System; using Xunit; -using Microsoft.Git.CredentialManager.SecureStorage; +using Microsoft.Git.CredentialManager.Interop.MacOS; -namespace Microsoft.Git.CredentialManager.Tests.SecureStorage +namespace Microsoft.Git.CredentialManager.Tests.Interop.MacOS { public class MacOSKeychainTests { diff --git a/src/shared/Microsoft.Git.CredentialManager.Tests/SecureStorage/WindowsCredentialManagerTests.cs b/src/shared/Microsoft.Git.CredentialManager.Tests/Interop/Windows/WindowsCredentialManagerTests.cs similarity index 94% rename from src/shared/Microsoft.Git.CredentialManager.Tests/SecureStorage/WindowsCredentialManagerTests.cs rename to src/shared/Microsoft.Git.CredentialManager.Tests/Interop/Windows/WindowsCredentialManagerTests.cs index d76ac9e59..0868a14ca 100644 --- a/src/shared/Microsoft.Git.CredentialManager.Tests/SecureStorage/WindowsCredentialManagerTests.cs +++ b/src/shared/Microsoft.Git.CredentialManager.Tests/Interop/Windows/WindowsCredentialManagerTests.cs @@ -2,9 +2,9 @@ // Licensed under the MIT license. using System; using Xunit; -using Microsoft.Git.CredentialManager.SecureStorage; +using Microsoft.Git.CredentialManager.Interop.Windows; -namespace Microsoft.Git.CredentialManager.Tests.SecureStorage +namespace Microsoft.Git.CredentialManager.Tests.Interop.Windows { public class WindowsCredentialManagerTests { diff --git a/src/shared/Microsoft.Git.CredentialManager/Application.cs b/src/shared/Microsoft.Git.CredentialManager/Application.cs index e6baccf70..4d5d543d8 100644 --- a/src/shared/Microsoft.Git.CredentialManager/Application.cs +++ b/src/shared/Microsoft.Git.CredentialManager/Application.cs @@ -4,7 +4,6 @@ using System.ComponentModel; using System.Threading.Tasks; using Microsoft.Git.CredentialManager.Commands; -using Microsoft.Git.CredentialManager.SecureStorage; namespace Microsoft.Git.CredentialManager { diff --git a/src/shared/Microsoft.Git.CredentialManager/CommandContext.cs b/src/shared/Microsoft.Git.CredentialManager/CommandContext.cs index 32d378ea5..29df0ef19 100644 --- a/src/shared/Microsoft.Git.CredentialManager/CommandContext.cs +++ b/src/shared/Microsoft.Git.CredentialManager/CommandContext.cs @@ -5,7 +5,8 @@ using System.Collections.Generic; using System.IO; using System.Text; -using Microsoft.Git.CredentialManager.SecureStorage; +using Microsoft.Git.CredentialManager.Interop.MacOS; +using Microsoft.Git.CredentialManager.Interop.Windows; namespace Microsoft.Git.CredentialManager { diff --git a/src/shared/Microsoft.Git.CredentialManager/Commands/EraseCommand.cs b/src/shared/Microsoft.Git.CredentialManager/Commands/EraseCommand.cs index 272c32990..3ebc66ee3 100644 --- a/src/shared/Microsoft.Git.CredentialManager/Commands/EraseCommand.cs +++ b/src/shared/Microsoft.Git.CredentialManager/Commands/EraseCommand.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. using System; using System.Threading.Tasks; -using Microsoft.Git.CredentialManager.SecureStorage; namespace Microsoft.Git.CredentialManager.Commands { diff --git a/src/shared/Microsoft.Git.CredentialManager/Commands/GetCommand.cs b/src/shared/Microsoft.Git.CredentialManager/Commands/GetCommand.cs index ef3eada49..73895571c 100644 --- a/src/shared/Microsoft.Git.CredentialManager/Commands/GetCommand.cs +++ b/src/shared/Microsoft.Git.CredentialManager/Commands/GetCommand.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. using System.Collections.Generic; using System.Threading.Tasks; -using Microsoft.Git.CredentialManager.SecureStorage; namespace Microsoft.Git.CredentialManager.Commands { diff --git a/src/shared/Microsoft.Git.CredentialManager/GitCredential.cs b/src/shared/Microsoft.Git.CredentialManager/Credential.cs similarity index 67% rename from src/shared/Microsoft.Git.CredentialManager/GitCredential.cs rename to src/shared/Microsoft.Git.CredentialManager/Credential.cs index 2b6dcc95d..78cf59490 100644 --- a/src/shared/Microsoft.Git.CredentialManager/GitCredential.cs +++ b/src/shared/Microsoft.Git.CredentialManager/Credential.cs @@ -6,10 +6,26 @@ namespace Microsoft.Git.CredentialManager { + /// + /// Represents a simple credential; user name and password pair. + /// + public interface ICredential + { + /// + /// User name. + /// + string UserName { get; } + + /// + /// Password. + /// + string Password { get; } + } + /// /// Represents a credential (username/password pair) that Git can use to authenticate to a remote repository. /// - public class GitCredential : SecureStorage.ICredential + public class GitCredential : ICredential { public GitCredential(string userName, string password) { @@ -17,22 +33,19 @@ public GitCredential(string userName, string password) Password = password; } - /// - /// User name. - /// public string UserName { get; } - /// - /// Password. - /// public string Password { get; } + } + public static class CredentialExtensions + { /// - /// Returns the base-64 encoded, {username}:{password} formatted string of this ``. + /// Returns the base-64 encoded, {username}:{password} formatted string of this ``. /// - public string ToBase64String() + public static string ToBase64String(this ICredential credential) { - string basicAuthValue = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", UserName, Password); + string basicAuthValue = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", credential.UserName, credential.Password); byte[] authBytes = Encoding.UTF8.GetBytes(basicAuthValue); return Convert.ToBase64String(authBytes); } diff --git a/src/shared/Microsoft.Git.CredentialManager/SecureStorage/ICredentialStore.cs b/src/shared/Microsoft.Git.CredentialManager/ICredentialStore.cs similarity index 95% rename from src/shared/Microsoft.Git.CredentialManager/SecureStorage/ICredentialStore.cs rename to src/shared/Microsoft.Git.CredentialManager/ICredentialStore.cs index ec59dc4d1..c458a3836 100644 --- a/src/shared/Microsoft.Git.CredentialManager/SecureStorage/ICredentialStore.cs +++ b/src/shared/Microsoft.Git.CredentialManager/ICredentialStore.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Git.CredentialManager.SecureStorage + +namespace Microsoft.Git.CredentialManager { /// /// Represents a secure storage location for s. diff --git a/src/shared/Microsoft.Git.CredentialManager/SecureStorage/NativeMethods.cs b/src/shared/Microsoft.Git.CredentialManager/Interop/InteropUtils.cs similarity index 79% rename from src/shared/Microsoft.Git.CredentialManager/SecureStorage/NativeMethods.cs rename to src/shared/Microsoft.Git.CredentialManager/Interop/InteropUtils.cs index d58675347..98556d6be 100644 --- a/src/shared/Microsoft.Git.CredentialManager/SecureStorage/NativeMethods.cs +++ b/src/shared/Microsoft.Git.CredentialManager/Interop/InteropUtils.cs @@ -3,9 +3,9 @@ using System; using System.Runtime.InteropServices; -namespace Microsoft.Git.CredentialManager.SecureStorage +namespace Microsoft.Git.CredentialManager.Interop { - internal static partial class NativeMethods + internal static class InteropUtils { public static byte[] ToByteArray(IntPtr ptr, long count) { diff --git a/src/shared/Microsoft.Git.CredentialManager/SecureStorage/MacOSKeychain.cs b/src/shared/Microsoft.Git.CredentialManager/Interop/MacOS/MacOSKeychain.cs similarity index 92% rename from src/shared/Microsoft.Git.CredentialManager/SecureStorage/MacOSKeychain.cs rename to src/shared/Microsoft.Git.CredentialManager/Interop/MacOS/MacOSKeychain.cs index a2eea312d..daebe77db 100644 --- a/src/shared/Microsoft.Git.CredentialManager/SecureStorage/MacOSKeychain.cs +++ b/src/shared/Microsoft.Git.CredentialManager/Interop/MacOS/MacOSKeychain.cs @@ -4,9 +4,10 @@ using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; -using static Microsoft.Git.CredentialManager.SecureStorage.NativeMethods.MacOS; +using Microsoft.Git.CredentialManager.Interop.MacOS.Native; +using static Microsoft.Git.CredentialManager.Interop.MacOS.Native.SecurityFramework; -namespace Microsoft.Git.CredentialManager.SecureStorage +namespace Microsoft.Git.CredentialManager.Interop.MacOS { public class MacOSKeychain : ICredentialStore { @@ -50,10 +51,10 @@ public ICredential Get(string key) string userName = Encoding.UTF8.GetString(userNameBytes); // Decode the password from the raw data - byte[] passwordBytes = NativeMethods.ToByteArray(passwordData, passwordLength); + byte[] passwordBytes = InteropUtils.ToByteArray(passwordData, passwordLength); string password = Encoding.UTF8.GetString(passwordBytes); - return new Credential(userName, password); + return new GitCredential(userName, password); case ErrorSecItemNotFound: return null; @@ -72,7 +73,7 @@ public ICredential Get(string key) if (itemRef != IntPtr.Zero) { - CFRelease(itemRef); + CoreFoundation.CFRelease(itemRef); } } } @@ -124,7 +125,7 @@ public void AddOrUpdate(string key, ICredential credential) if (itemRef != IntPtr.Zero) { - CFRelease(itemRef); + CoreFoundation.CFRelease(itemRef); } } } @@ -165,7 +166,7 @@ public bool Remove(string key) if (itemRef != IntPtr.Zero) { - CFRelease(itemRef); + CoreFoundation.CFRelease(itemRef); } } } @@ -207,7 +208,7 @@ private static byte[] GetAccountNameAttributeData(IntPtr itemRef) SecKeychainAttribute attribute = Marshal.PtrToStructure(attrList.Attributes); - return NativeMethods.ToByteArray(attribute.Data, attribute.Length); + return InteropUtils.ToByteArray(attribute.Data, attribute.Length); } finally { diff --git a/src/shared/Microsoft.Git.CredentialManager/Interop/MacOS/Native/CoreFoundation.cs b/src/shared/Microsoft.Git.CredentialManager/Interop/MacOS/Native/CoreFoundation.cs new file mode 100644 index 000000000..93cb6e8c0 --- /dev/null +++ b/src/shared/Microsoft.Git.CredentialManager/Interop/MacOS/Native/CoreFoundation.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.Git.CredentialManager.Interop.MacOS.Native +{ + public static class CoreFoundation + { + private const string CoreFoundationFrameworkLib = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation"; + + [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern void CFRelease(IntPtr cf); + } +} diff --git a/src/shared/Microsoft.Git.CredentialManager/Interop/MacOS/Native/SecurityFramework.cs b/src/shared/Microsoft.Git.CredentialManager/Interop/MacOS/Native/SecurityFramework.cs new file mode 100644 index 000000000..06e1d7aff --- /dev/null +++ b/src/shared/Microsoft.Git.CredentialManager/Interop/MacOS/Native/SecurityFramework.cs @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.Git.CredentialManager.Interop.MacOS.Native +{ + // https://developer.apple.com/documentation/security/keychain_services/keychain_items + public static class SecurityFramework + { + private const string SecurityFrameworkLib = "/System/Library/Frameworks/Security.framework/Security"; + + [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern int SecKeychainAddGenericPassword( + IntPtr keychain, + uint serviceNameLength, + string serviceName, + uint accountNameLength, + string accountName, + uint passwordLength, + byte[] passwordData, + out IntPtr itemRef); + + [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern int SecKeychainFindGenericPassword( + IntPtr keychainOrArray, + uint serviceNameLength, + string serviceName, + uint accountNameLength, + string accountName, + out uint passwordLength, + out IntPtr passwordData, + out IntPtr itemRef); + + [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern int SecKeychainItemCopyAttributesAndData( + IntPtr itemRef, + ref SecKeychainAttributeInfo info, + IntPtr itemClass, // SecItemClass* + out IntPtr attrList, // SecKeychainAttributeList* + out uint dataLength, + IntPtr data); + + [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern int SecKeychainItemModifyAttributesAndData( + IntPtr itemRef, + IntPtr attrList, // SecKeychainAttributeList* + uint length, + byte[] data); + + [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern int SecKeychainItemDelete( + IntPtr itemRef); + + [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern int SecKeychainItemFreeContent( + IntPtr attrList, // SecKeychainAttributeList* + IntPtr data); + + [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern int SecKeychainItemFreeAttributesAndData( + IntPtr attrList, // SecKeychainAttributeList* + IntPtr data); + + // https://developer.apple.com/documentation/security/1542001-security_framework_result_codes + public const int OK = 0; + public const int ErrorSecNoSuchKeychain = -25294; + public const int ErrorSecInvalidKeychain = -25295; + public const int ErrorSecAuthFailed = -25293; + public const int ErrorSecDuplicateItem = -25299; + public const int ErrorSecItemNotFound = -25300; + public const int ErrorSecInteractionNotAllowed = -25308; + public const int ErrorSecInteractionRequired = -25315; + public const int ErrorSecNoSuchAttr = -25303; + + public static void ThrowIfError(int error, string defaultErrorMessage = "Unknown error.") + { + switch (error) + { + case OK: + return; + case ErrorSecNoSuchKeychain: + throw new InvalidOperationException($"The keychain does not exist. ({ErrorSecNoSuchKeychain})"); + case ErrorSecInvalidKeychain: + throw new InvalidOperationException($"The keychain is not valid. ({ErrorSecInvalidKeychain})"); + case ErrorSecAuthFailed: + throw new InvalidOperationException($"Authorization/Authentication failed. ({ErrorSecAuthFailed})"); + case ErrorSecDuplicateItem: + throw new InvalidOperationException($"The item already exists. ({ErrorSecDuplicateItem})"); + case ErrorSecItemNotFound: + throw new InvalidOperationException($"The item cannot be found. ({ErrorSecItemNotFound})"); + case ErrorSecInteractionNotAllowed: + throw new InvalidOperationException($"Interaction with the Security Server is not allowed. ({ErrorSecInteractionNotAllowed})"); + case ErrorSecInteractionRequired: + throw new InvalidOperationException($"User interaction is required. ({ErrorSecInteractionRequired})"); + case ErrorSecNoSuchAttr: + throw new InvalidOperationException($"The attribute does not exist. ({ErrorSecNoSuchAttr})"); + default: + throw new Exception($"{defaultErrorMessage} ({error})"); + } + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct SecKeychainAttributeInfo + { + public uint Count; + public IntPtr Tag; // uint* (SecKeychainAttrType*) + public IntPtr Format; // uint* (CssmDbAttributeFormat*) + } + + [StructLayout(LayoutKind.Sequential)] + public struct SecKeychainAttributeList + { + public uint Count; + public IntPtr Attributes; // SecKeychainAttribute* + } + + [StructLayout(LayoutKind.Sequential)] + public struct SecKeychainAttribute + { + public SecKeychainAttrType Tag; + public uint Length; + public IntPtr Data; + } + + public enum CssmDbAttributeFormat : uint + { + String = 0, + SInt32 = 1, + UInt32 = 2, + BigNum = 3, + Real = 4, + TimeDate = 5, + Blob = 6, + MultiUInt32 = 7, + Complex = 8 + }; + + public enum SecKeychainAttrType : uint + { + // https://developer.apple.com/documentation/security/secitemattr/accountitemattr + AccountItem = 1633903476, + } +} diff --git a/src/shared/Microsoft.Git.CredentialManager/Interop/Windows/Native/Advapi32.cs b/src/shared/Microsoft.Git.CredentialManager/Interop/Windows/Native/Advapi32.cs new file mode 100644 index 000000000..d859cc1a4 --- /dev/null +++ b/src/shared/Microsoft.Git.CredentialManager/Interop/Windows/Native/Advapi32.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME; + +namespace Microsoft.Git.CredentialManager.Interop.Windows.Native +{ + // https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ + public static class Advapi32 + { + [DllImport("advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool CredRead( + string target, + CredentialType type, + int reserved, + out IntPtr credential); + + [DllImport("advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool CredWrite( + ref Win32Credential credential, + int flags); + + [DllImport("advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool CredDelete( + string target, + CredentialType type, + int flags); + + [DllImport("advapi32.dll", EntryPoint = "CredFree", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern void CredFree( + IntPtr credential); + } + + public enum CredentialType + { + Generic = 1, + DomainPassword = 2, + DomainCertificate = 3, + DomainVisiblePassword = 4, + } + + public enum CredentialPersist + { + Session = 1, + LocalMachine = 2, + Enterprise = 3, + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct Win32Credential + { + public int Flags; + public CredentialType Type; + [MarshalAs(UnmanagedType.LPWStr)] + public string TargetName; + [MarshalAs(UnmanagedType.LPWStr)] + public string Comment; + public FILETIME LastWritten; + public int CredentialBlobSize; + public IntPtr CredentialBlob; + public CredentialPersist Persist; + public int AttributeCount; + public IntPtr CredAttribute; + [MarshalAs(UnmanagedType.LPWStr)] + public string TargetAlias; + [MarshalAs(UnmanagedType.LPWStr)] + public string UserName; + } +} diff --git a/src/shared/Microsoft.Git.CredentialManager/Interop/Windows/Native/Common.cs b/src/shared/Microsoft.Git.CredentialManager/Interop/Windows/Native/Common.cs new file mode 100644 index 000000000..c5771513d --- /dev/null +++ b/src/shared/Microsoft.Git.CredentialManager/Interop/Windows/Native/Common.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; + +namespace Microsoft.Git.CredentialManager.Interop.Windows.Native +{ + public static class Common + { + // https://docs.microsoft.com/en-gb/windows/desktop/Debug/system-error-codes + public const int OK = 0; + public const int ERROR_NO_SUCH_LOGON_SESSION = 0x520; + public const int ERROR_NOT_FOUND = 0x490; + public const int ERROR_BAD_USERNAME = 0x89A; + public const int ERROR_INVALID_FLAGS = 0x3EC; + public const int ERROR_INVALID_PARAMETER = 0x57; + + public static int GetLastError(bool success) + { + if (success) + { + return OK; + } + + return Marshal.GetLastWin32Error(); + } + + public static void ThrowIfError(int error, string defaultErrorMessage = null) + { + switch (error) + { + case OK: + return; + default: + // The Win32Exception constructor will automatically get the human-readable + // message for the error code. + throw new Exception(defaultErrorMessage, new Win32Exception(error)); + } + } + } +} diff --git a/src/shared/Microsoft.Git.CredentialManager/SecureStorage/WindowsCredentialManager.cs b/src/shared/Microsoft.Git.CredentialManager/Interop/Windows/WindowsCredentialManager.cs similarity index 71% rename from src/shared/Microsoft.Git.CredentialManager/SecureStorage/WindowsCredentialManager.cs rename to src/shared/Microsoft.Git.CredentialManager/Interop/Windows/WindowsCredentialManager.cs index 4b7ac4819..34af187f3 100644 --- a/src/shared/Microsoft.Git.CredentialManager/SecureStorage/WindowsCredentialManager.cs +++ b/src/shared/Microsoft.Git.CredentialManager/Interop/Windows/WindowsCredentialManager.cs @@ -3,9 +3,9 @@ using System; using System.Runtime.InteropServices; using System.Text; -using static Microsoft.Git.CredentialManager.SecureStorage.NativeMethods.Windows; +using Microsoft.Git.CredentialManager.Interop.Windows.Native; -namespace Microsoft.Git.CredentialManager.SecureStorage +namespace Microsoft.Git.CredentialManager.Interop.Windows { public class WindowsCredentialManager : ICredentialStore { @@ -35,27 +35,27 @@ public ICredential Get(string key) try { - int result = GetLastError( - CredRead(key, CredentialType.Generic, 0, out credPtr) + int result = Common.GetLastError( + Advapi32.CredRead(key, CredentialType.Generic, 0, out credPtr) ); switch (result) { - case OK: + case Common.OK: Win32Credential credential = Marshal.PtrToStructure(credPtr); var userName = credential.UserName; - byte[] passwordBytes = NativeMethods.ToByteArray(credential.CredentialBlob, credential.CredentialBlobSize); + byte[] passwordBytes = InteropUtils.ToByteArray(credential.CredentialBlob, credential.CredentialBlobSize); var password = Encoding.Unicode.GetString(passwordBytes); - return new Credential(userName, password); + return new GitCredential(userName, password); - case ERROR_NOT_FOUND: + case Common.ERROR_NOT_FOUND: return null; default: - ThrowIfError(result, "Failed to read item from store."); + Common.ThrowIfError(result, "Failed to read item from store."); return null; } } @@ -63,7 +63,7 @@ public ICredential Get(string key) { if (credPtr != IntPtr.Zero) { - CredFree(credPtr); + Advapi32.CredFree(credPtr); } } } @@ -87,11 +87,11 @@ public void AddOrUpdate(string key, ICredential credential) { Marshal.Copy(passwordBytes, 0, w32Credential.CredentialBlob, passwordBytes.Length); - int result = GetLastError( - CredWrite(ref w32Credential, 0) + int result = Common.GetLastError( + Advapi32.CredWrite(ref w32Credential, 0) ); - ThrowIfError(result, "Failed to write item to store."); + Common.ThrowIfError(result, "Failed to write item to store."); } finally { @@ -104,20 +104,20 @@ public void AddOrUpdate(string key, ICredential credential) public bool Remove(string key) { - int result = GetLastError( - CredDelete(key, CredentialType.Generic, 0) + int result = Common.GetLastError( + Advapi32.CredDelete(key, CredentialType.Generic, 0) ); switch (result) { - case OK: + case Common.OK: return true; - case ERROR_NOT_FOUND: + case Common.ERROR_NOT_FOUND: return false; default: - ThrowIfError(result); + Common.ThrowIfError(result); return false; } } diff --git a/src/shared/Microsoft.Git.CredentialManager/SecureStorage/Credential.cs b/src/shared/Microsoft.Git.CredentialManager/SecureStorage/Credential.cs deleted file mode 100644 index 3e1700aee..000000000 --- a/src/shared/Microsoft.Git.CredentialManager/SecureStorage/Credential.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. -namespace Microsoft.Git.CredentialManager.SecureStorage -{ - /// - /// Represents a simple credential; user name and password pair. - /// - public interface ICredential - { - string UserName { get; } - string Password { get; } - } - - internal class Credential : ICredential - { - public Credential(string userName, string password) - { - UserName = userName; - Password = password; - } - - public string UserName { get; } - public string Password { get; } - } -} diff --git a/src/shared/Microsoft.Git.CredentialManager/SecureStorage/NativeMethods.Mac.cs b/src/shared/Microsoft.Git.CredentialManager/SecureStorage/NativeMethods.Mac.cs deleted file mode 100644 index cb648df99..000000000 --- a/src/shared/Microsoft.Git.CredentialManager/SecureStorage/NativeMethods.Mac.cs +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. -using System; -using System.Runtime.InteropServices; - -namespace Microsoft.Git.CredentialManager.SecureStorage -{ - internal static partial class NativeMethods - { - // https://developer.apple.com/documentation/security/keychain_services/keychain_items - public static class MacOS - { - private const string CoreFoundationFrameworkLib = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation"; - private const string SecurityFrameworkLib = "/System/Library/Frameworks/Security.framework/Security"; - - // https://developer.apple.com/documentation/security/1542001-security_framework_result_codes - public const int OK = 0; - public const int ErrorSecNoSuchKeychain = -25294; - public const int ErrorSecInvalidKeychain = -25295; - public const int ErrorSecAuthFailed = -25293; - public const int ErrorSecDuplicateItem = -25299; - public const int ErrorSecItemNotFound = -25300; - public const int ErrorSecInteractionNotAllowed = -25308; - public const int ErrorSecInteractionRequired = -25315; - public const int ErrorSecNoSuchAttr = -25303; - - public static void ThrowIfError(int error, string defaultErrorMessage = "Unknown error.") - { - switch (error) - { - case OK: - return; - case ErrorSecNoSuchKeychain: - throw new InvalidOperationException($"The keychain does not exist. ({ErrorSecNoSuchKeychain})"); - case ErrorSecInvalidKeychain: - throw new InvalidOperationException($"The keychain is not valid. ({ErrorSecInvalidKeychain})"); - case ErrorSecAuthFailed: - throw new InvalidOperationException($"Authorization/Authentication failed. ({ErrorSecAuthFailed})"); - case ErrorSecDuplicateItem: - throw new InvalidOperationException($"The item already exists. ({ErrorSecDuplicateItem})"); - case ErrorSecItemNotFound: - throw new InvalidOperationException($"The item cannot be found. ({ErrorSecItemNotFound})"); - case ErrorSecInteractionNotAllowed: - throw new InvalidOperationException($"Interaction with the Security Server is not allowed. ({ErrorSecInteractionNotAllowed})"); - case ErrorSecInteractionRequired: - throw new InvalidOperationException($"User interaction is required. ({ErrorSecInteractionRequired})"); - case ErrorSecNoSuchAttr: - throw new InvalidOperationException($"The attribute does not exist. ({ErrorSecNoSuchAttr})"); - default: - throw new Exception($"{defaultErrorMessage} ({error})"); - } - } - - [StructLayout(LayoutKind.Sequential)] - public struct SecKeychainAttributeInfo - { - public uint Count; - public IntPtr Tag; // uint* (SecKeychainAttrType*) - public IntPtr Format; // uint* (CssmDbAttributeFormat*) - } - - [StructLayout(LayoutKind.Sequential)] - public struct SecKeychainAttributeList - { - public uint Count; - public IntPtr Attributes; // SecKeychainAttribute* - } - - [StructLayout(LayoutKind.Sequential)] - public struct SecKeychainAttribute - { - public SecKeychainAttrType Tag; - public uint Length; - public IntPtr Data; - } - - public enum CssmDbAttributeFormat : uint - { - String = 0, - SInt32 = 1, - UInt32 = 2, - BigNum = 3, - Real = 4, - TimeDate = 5, - Blob = 6, - MultiUInt32 = 7, - Complex = 8 - }; - - public enum SecKeychainAttrType : uint - { - // https://developer.apple.com/documentation/security/secitemattr/accountitemattr - AccountItem = 1633903476, - } - - [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern void CFRelease(IntPtr cf); - - [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern int SecKeychainAddGenericPassword( - IntPtr keychain, - uint serviceNameLength, - string serviceName, - uint accountNameLength, - string accountName, - uint passwordLength, - byte[] passwordData, - out IntPtr itemRef); - - [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern int SecKeychainFindGenericPassword( - IntPtr keychainOrArray, - uint serviceNameLength, - string serviceName, - uint accountNameLength, - string accountName, - out uint passwordLength, - out IntPtr passwordData, - out IntPtr itemRef); - - [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern int SecKeychainItemCopyAttributesAndData( - IntPtr itemRef, - ref SecKeychainAttributeInfo info, - IntPtr itemClass, // SecItemClass* - out IntPtr attrList, // SecKeychainAttributeList* - out uint dataLength, - IntPtr data); - - [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern int SecKeychainItemModifyAttributesAndData( - IntPtr itemRef, - IntPtr attrList, // SecKeychainAttributeList* - uint length, - byte[] data); - - [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern int SecKeychainItemDelete( - IntPtr itemRef); - - [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern int SecKeychainItemFreeContent( - IntPtr attrList, // SecKeychainAttributeList* - IntPtr data); - - [DllImport(SecurityFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern int SecKeychainItemFreeAttributesAndData( - IntPtr attrList, // SecKeychainAttributeList* - IntPtr data); - - } - } -} diff --git a/src/shared/Microsoft.Git.CredentialManager/SecureStorage/NativeMethods.Windows.cs b/src/shared/Microsoft.Git.CredentialManager/SecureStorage/NativeMethods.Windows.cs deleted file mode 100644 index 307c85cca..000000000 --- a/src/shared/Microsoft.Git.CredentialManager/SecureStorage/NativeMethods.Windows.cs +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. -using System; -using System.ComponentModel; -using System.Runtime.InteropServices; -using FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME; - -namespace Microsoft.Git.CredentialManager.SecureStorage -{ - internal static partial class NativeMethods - { - // https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ - public static class Windows - { - private const string Advapi32 = "advapi32.dll"; - - // https://docs.microsoft.com/en-gb/windows/desktop/Debug/system-error-codes - public const int OK = 0; - public const int ERROR_NO_SUCH_LOGON_SESSION = 0x520; - public const int ERROR_NOT_FOUND = 0x490; - public const int ERROR_BAD_USERNAME = 0x89A; - public const int ERROR_INVALID_FLAGS = 0x3EC; - public const int ERROR_INVALID_PARAMETER = 0x57; - - public static int GetLastError(bool success) - { - if (success) - { - return OK; - } - - return Marshal.GetLastWin32Error(); - } - - public static void ThrowIfError(int error, string defaultErrorMessage = null) - { - switch (error) - { - case OK: - return; - default: - // The Win32Exception constructor will automatically get the human-readable - // message for the error code. - throw new Exception(defaultErrorMessage, new Win32Exception(error)); - } - } - - public enum CredentialType - { - Generic = 1, - DomainPassword = 2, - DomainCertificate = 3, - DomainVisiblePassword = 4, - } - - public enum CredentialPersist - { - Session = 1, - LocalMachine = 2, - Enterprise = 3, - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - public struct Win32Credential - { - public int Flags; - public CredentialType Type; - [MarshalAs(UnmanagedType.LPWStr)] public string TargetName; - [MarshalAs(UnmanagedType.LPWStr)] public string Comment; - public FILETIME LastWritten; - public int CredentialBlobSize; - public IntPtr CredentialBlob; - public CredentialPersist Persist; - public int AttributeCount; - public IntPtr CredAttribute; - [MarshalAs(UnmanagedType.LPWStr)] public string TargetAlias; - [MarshalAs(UnmanagedType.LPWStr)] public string UserName; - } - - [DllImport(Advapi32, EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern bool CredRead( - string target, - CredentialType type, - int reserved, - out IntPtr credential); - - [DllImport(Advapi32, EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern bool CredWrite( - ref Win32Credential credential, - int flags); - - [DllImport(Advapi32, EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern bool CredDelete( - string target, - CredentialType type, - int flags); - - [DllImport(Advapi32, EntryPoint = "CredFree", CharSet = CharSet.Unicode, SetLastError = true)] - internal static extern void CredFree( - IntPtr credential); - } - } -} diff --git a/src/shared/TestInfrastructure/Objects/TestCommandContext.cs b/src/shared/TestInfrastructure/Objects/TestCommandContext.cs index 0019ffe6f..0222af349 100644 --- a/src/shared/TestInfrastructure/Objects/TestCommandContext.cs +++ b/src/shared/TestInfrastructure/Objects/TestCommandContext.cs @@ -5,7 +5,6 @@ using System.Collections.ObjectModel; using System.IO; using System.Text; -using Microsoft.Git.CredentialManager.SecureStorage; namespace Microsoft.Git.CredentialManager.Tests.Objects { diff --git a/src/shared/TestInfrastructure/Objects/TestCredentialStore.cs b/src/shared/TestInfrastructure/Objects/TestCredentialStore.cs index c6b268a76..bbd08c4b7 100644 --- a/src/shared/TestInfrastructure/Objects/TestCredentialStore.cs +++ b/src/shared/TestInfrastructure/Objects/TestCredentialStore.cs @@ -3,7 +3,6 @@ using System; using System.Collections; using System.Collections.Generic; -using Microsoft.Git.CredentialManager.SecureStorage; namespace Microsoft.Git.CredentialManager.Tests.Objects {