Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add secure credential storage APIs for Windows+macOS #2

Merged
merged 7 commits into from
Dec 7, 2018
Merged
28 changes: 28 additions & 0 deletions src/Microsoft.Git.CredentialManager/CommandContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.Git.CredentialManager.SecureStorage;

namespace Microsoft.Git.CredentialManager
{
Expand Down Expand Up @@ -37,6 +38,11 @@ public interface ICommandContext
/// </summary>
IFileSystem FileSystem { get; }

/// <summary>
/// Secure credential storage.
/// </summary>
ICredentialStore CredentialStore { get; }

/// <summary>
/// Access the environment variables for the current GCM process.
/// </summary>
Expand Down Expand Up @@ -110,6 +116,8 @@ public TextWriter StdError

public IFileSystem FileSystem { get; } = new FileSystem();

public ICredentialStore CredentialStore { get; } = CreateCredentialStore();

public IReadOnlyDictionary<string, string> GetEnvironmentVariables()
{
IDictionary variables = Environment.GetEnvironmentVariables();
Expand Down Expand Up @@ -140,6 +148,26 @@ public IReadOnlyDictionary<string, string> GetEnvironmentVariables()
}

#endregion

private static ICredentialStore CreateCredentialStore()
{
if (PlatformUtils.IsMacOS())
{
return MacOSKeychain.Open();
}

if (PlatformUtils.IsWindows())
{
return WindowsCredentialManager.Open();
}

if (PlatformUtils.IsLinux())
{
throw new NotImplementedException();
}

throw new PlatformNotSupportedException();
}
}

public static class CommandContextExtensions
Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.Git.CredentialManager/GitCredential.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Microsoft.Git.CredentialManager
/// <summary>
/// Represents a credential (username/password pair) that Git can use to authenticate to a remote repository.
/// </summary>
public class GitCredential
public class GitCredential : SecureStorage.ICredential
{
public GitCredential(string userName, string password)
{
Expand Down
23 changes: 23 additions & 0 deletions src/Microsoft.Git.CredentialManager/SecureStorage/Credential.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Microsoft.Git.CredentialManager.SecureStorage
{
/// <summary>
/// Represents a simple credential; user name and password pair.
/// </summary>
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; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace Microsoft.Git.CredentialManager.SecureStorage
{
/// <summary>
/// Represents a secure storage location for <see cref="ICredential"/>s.
/// </summary>
public interface ICredentialStore
{
/// <summary>
/// Get credential from the store with the specified key.
/// </summary>
/// <param name="key">Key for credential to retrieve.</param>
/// <returns>Stored credential or null if not found.</returns>
ICredential Get(string key);

/// <summary>
/// Add or update credential in the store with the specified key.
/// </summary>
/// <param name="key">Key for credential to add/update.</param>
/// <param name="credential">Credential to store.</param>
void AddOrUpdate(string key, ICredential credential);

/// <summary>
/// Delete credential from the store with the specified key.
/// </summary>
/// <param name="key">Key of credential to delete.</param>
/// <returns>True if the credential was deleted, false otherwise.</returns>
bool Remove(string key);
}
}
231 changes: 231 additions & 0 deletions src/Microsoft.Git.CredentialManager/SecureStorage/MacOSKeychain.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using static Microsoft.Git.CredentialManager.SecureStorage.NativeMethods.MacOS;

namespace Microsoft.Git.CredentialManager.SecureStorage
{
public class MacOSKeychain : ICredentialStore
{
#region Constructors

/// <summary>
/// Open the default keychain (current user's login keychain).
/// </summary>
/// <returns>Default keychain.</returns>
public static MacOSKeychain Open()
{
return new MacOSKeychain();
}

private MacOSKeychain()
{
PlatformUtils.EnsureMacOS();
}

#endregion

#region ICredentialStore

public ICredential Get(string key)
{
IntPtr passwordData = IntPtr.Zero;
IntPtr itemRef = IntPtr.Zero;

try
{
// Find the item (itemRef) and password (passwordData) in the keychain
int findResult = SecKeychainFindGenericPassword(
IntPtr.Zero, (uint) key.Length, key, 0, null,
out uint passwordLength, out passwordData, out itemRef);

switch (findResult)
{
case OK:
// Get and decode the user name from the 'account name' attribute
byte[] userNameBytes = GetAccountNameAttributeData(itemRef);
string userName = Encoding.UTF8.GetString(userNameBytes);

// Decode the password from the raw data
byte[] passwordBytes = NativeMethods.ToByteArray(passwordData, passwordLength);
string password = Encoding.UTF8.GetString(passwordBytes);

return new Credential(userName, password);

case ErrorSecItemNotFound:
return null;

default:
ThrowIfError(findResult);
return null;
}
}
finally
{
if (passwordData != IntPtr.Zero)
{
SecKeychainItemFreeContent(IntPtr.Zero, passwordData);
}

if (itemRef != IntPtr.Zero)
{
CFRelease(itemRef);
}
}
}

public void AddOrUpdate(string key, ICredential credential)
{
byte[] passwordBytes = Encoding.UTF8.GetBytes(credential.Password);

IntPtr passwordData = IntPtr.Zero;
IntPtr itemRef = IntPtr.Zero;

try
{
// Check if an entry already exists in the keychain
int findResult = SecKeychainFindGenericPassword(
IntPtr.Zero, (uint) key.Length, key, (uint) credential.UserName.Length, credential.UserName,
out uint _, out passwordData, out itemRef);

switch (findResult)
{
// Create new entry
case OK:
ThrowIfError(
SecKeychainItemModifyAttributesAndData(itemRef, IntPtr.Zero, (uint) passwordBytes.Length, passwordBytes),
"Could not update existing item"
);
break;

// Update existing entry
case ErrorSecItemNotFound:
ThrowIfError(
SecKeychainAddGenericPassword(IntPtr.Zero, (uint) key.Length, key, (uint) credential.UserName.Length,
credential.UserName, (uint) passwordBytes.Length, passwordBytes, out itemRef),
"Could not create new item"
);
break;

default:
ThrowIfError(findResult);
break;
}
}
finally
{
if (passwordData != IntPtr.Zero)
{
SecKeychainItemFreeContent(IntPtr.Zero, passwordData);
}

if (itemRef != IntPtr.Zero)
{
CFRelease(itemRef);
}
}
}

public bool Remove(string key)
{
IntPtr passwordData = IntPtr.Zero;
IntPtr itemRef = IntPtr.Zero;

try
{
int findResult = SecKeychainFindGenericPassword(
IntPtr.Zero, (uint) key.Length, key, 0, null,
out _, out passwordData, out itemRef);

switch (findResult)
{
case OK:
ThrowIfError(
SecKeychainItemDelete(itemRef)
);
return true;

case ErrorSecItemNotFound:
return false;

default:
ThrowIfError(findResult);
return false;
}
}
finally
{
if (passwordData != IntPtr.Zero)
{
SecKeychainItemFreeContent(IntPtr.Zero, passwordData);
}

if (itemRef != IntPtr.Zero)
{
CFRelease(itemRef);
}
}
}

#endregion

#region Private Methods

private static byte[] GetAccountNameAttributeData(IntPtr itemRef)
{
IntPtr tagArrayPtr = IntPtr.Zero;
IntPtr formatArrayPtr = IntPtr.Zero;
IntPtr attrListPtr = IntPtr.Zero; // SecKeychainAttributeList

try
{
// Extract the user name by querying for the item's 'account' attribute
tagArrayPtr = Marshal.AllocHGlobal(sizeof(SecKeychainAttrType));
Marshal.WriteInt32(tagArrayPtr, (int) SecKeychainAttrType.AccountItem);

formatArrayPtr = Marshal.AllocHGlobal(sizeof(CssmDbAttributeFormat));
Marshal.WriteInt32(formatArrayPtr, (int) CssmDbAttributeFormat.String);

var attributeInfo = new SecKeychainAttributeInfo
{
Count = 1,
Tag = tagArrayPtr,
Format = formatArrayPtr,
};

ThrowIfError(
SecKeychainItemCopyAttributesAndData(
itemRef, ref attributeInfo,
IntPtr.Zero, out attrListPtr, out _, IntPtr.Zero)
);

SecKeychainAttributeList attrList = Marshal.PtrToStructure<SecKeychainAttributeList>(attrListPtr);
Debug.Assert(attrList.Count == 1, "Only expecting a list structure containing one attribute to be returned");

SecKeychainAttribute attribute = Marshal.PtrToStructure<SecKeychainAttribute>(attrList.Attributes);

return NativeMethods.ToByteArray(attribute.Data, attribute.Length);
}
finally
{
if (tagArrayPtr != IntPtr.Zero)
{
Marshal.FreeHGlobal(tagArrayPtr);
}

if (formatArrayPtr != IntPtr.Zero)
{
Marshal.FreeHGlobal(formatArrayPtr);
}

if (attrListPtr != IntPtr.Zero)
{
SecKeychainItemFreeAttributesAndData(attrListPtr, IntPtr.Zero);
}
}
}

#endregion
}
}
Loading