Credentials store API (#38)

* CredentialService initial impl with Win32 support

- Basic CredentialService APIs for Save, Read, Delete
- E2E unit tests for Credential Service
- Win32 implementation with unit tests

* Save Password support on Mac v1

- Basic keychain support on Mac using Interop with the KeyChain APIs
- All but 1 unit test passing. This will pass once API is changed, but checking this in with the existing API so that if we decide to alter behavior, we have a reference point.

* Remove Username from Credentials API

- Removed Username option from Credentials as this caused conflicting behavior on Mac vs Windows

* Cleanup Using Statements and add Copyright

* Linux CredentialStore Prototype

* Linux credential store support

- Full support for Linux credential store with tests

* Plumbed CredentialService into Program init

* Addressing Pull Request comments
This commit is contained in:
Kevin Cunnane
2016-09-06 18:12:39 -07:00
committed by GitHub
parent 76e7ea041c
commit 8ca88992be
41 changed files with 3120 additions and 165 deletions

View File

@@ -0,0 +1,16 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Win32
{
// TODO Replace this strings class with a resx file
internal class CredentialResources
{
public const string PasswordLengthExceeded = "The password has exceeded 512 bytes.";
public const string TargetRequiredForDelete = "Target must be specified to delete a credential.";
public const string TargetRequiredForLookup = "Target must be specified to check existance of a credential.";
public const string CredentialDisposed = "Win32Credential object is already disposed.";
}
}

View File

@@ -0,0 +1,113 @@
//
// Code originally from http://credentialmanagement.codeplex.com/,
// Licensed under the Apache License 2.0
//
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.InteropServices;
using Microsoft.SqlTools.EditorServices.Utility;
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Win32
{
public class CredentialSet: List<Win32Credential>, IDisposable
{
bool _disposed;
public CredentialSet()
{
}
public CredentialSet(string target)
: this()
{
if (string.IsNullOrEmpty(target))
{
throw new ArgumentNullException("target");
}
Target = target;
}
public string Target { get; set; }
public void Dispose()
{
Dispose(true);
// Prevent GC Collection since we have already disposed of this object
GC.SuppressFinalize(this);
}
~CredentialSet()
{
Dispose(false);
}
private void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
if (Count > 0)
{
ForEach(cred => cred.Dispose());
}
}
}
_disposed = true;
}
public CredentialSet Load()
{
LoadInternal();
return this;
}
private void LoadInternal()
{
uint count;
IntPtr pCredentials = IntPtr.Zero;
bool result = NativeMethods.CredEnumerateW(Target, 0, out count, out pCredentials);
if (!result)
{
Logger.Write(LogLevel.Error, string.Format("Win32Exception: {0}", new Win32Exception(Marshal.GetLastWin32Error()).ToString()));
return;
}
// Read in all of the pointers first
IntPtr[] ptrCredList = new IntPtr[count];
for (int i = 0; i < count; i++)
{
ptrCredList[i] = Marshal.ReadIntPtr(pCredentials, IntPtr.Size*i);
}
// Now let's go through all of the pointers in the list
// and create our Credential object(s)
List<NativeMethods.CriticalCredentialHandle> credentialHandles =
ptrCredList.Select(ptrCred => new NativeMethods.CriticalCredentialHandle(ptrCred)).ToList();
IEnumerable<Win32Credential> existingCredentials = credentialHandles
.Select(handle => handle.GetCredential())
.Select(nativeCredential =>
{
Win32Credential credential = new Win32Credential();
credential.LoadInternal(nativeCredential);
return credential;
});
AddRange(existingCredentials);
// The individual credentials should not be free'd
credentialHandles.ForEach(handle => handle.SetHandleAsInvalid());
// Clean up memory to the Enumeration pointer
NativeMethods.CredFree(pCredentials);
}
}
}

View File

@@ -0,0 +1,16 @@
//
// Code originally from http://credentialmanagement.codeplex.com/,
// Licensed under the Apache License 2.0
//
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Win32
{
public enum CredentialType: uint
{
None = 0,
Generic = 1,
DomainPassword = 2,
DomainCertificate = 3,
DomainVisiblePassword = 4
}
}

View File

@@ -0,0 +1,109 @@
//
// Code originally from http://credentialmanagement.codeplex.com/,
// Licensed under the Apache License 2.0
//
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Win32
{
internal class NativeMethods
{
[StructLayout(LayoutKind.Sequential)]
internal struct CREDENTIAL
{
public int Flags;
public int Type;
[MarshalAs(UnmanagedType.LPWStr)]
public string TargetName;
[MarshalAs(UnmanagedType.LPWStr)]
public string Comment;
public long LastWritten;
public int CredentialBlobSize;
public IntPtr CredentialBlob;
public int Persist;
public int AttributeCount;
public IntPtr Attributes;
[MarshalAs(UnmanagedType.LPWStr)]
public string TargetAlias;
[MarshalAs(UnmanagedType.LPWStr)]
public string UserName;
}
[DllImport("Advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern bool CredRead(string target, CredentialType type, int reservedFlag, out IntPtr CredentialPtr);
[DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern bool CredWrite([In] ref CREDENTIAL userCredential, [In] UInt32 flags);
[DllImport("Advapi32.dll", EntryPoint = "CredFree", SetLastError = true)]
internal static extern bool CredFree([In] IntPtr cred);
[DllImport("advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode)]
internal static extern bool CredDelete(StringBuilder target, CredentialType type, int flags);
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern bool CredEnumerateW(string filter, int flag, out uint count, out IntPtr pCredentials);
[DllImport("ole32.dll")]
internal static extern void CoTaskMemFree(IntPtr ptr);
internal abstract class CriticalHandleZeroOrMinusOneIsInvalid : CriticalHandle
{
protected CriticalHandleZeroOrMinusOneIsInvalid() : base(IntPtr.Zero)
{
}
public override bool IsInvalid
{
get { return handle == new IntPtr(0) || handle == new IntPtr(-1); }
}
}
internal sealed class CriticalCredentialHandle : CriticalHandleZeroOrMinusOneIsInvalid
{
// Set the handle.
internal CriticalCredentialHandle(IntPtr preexistingHandle)
{
SetHandle(preexistingHandle);
}
internal CREDENTIAL GetCredential()
{
if (!IsInvalid)
{
// Get the Credential from the mem location
return (CREDENTIAL)Marshal.PtrToStructure<CREDENTIAL>(handle);
}
else
{
throw new InvalidOperationException("Invalid CriticalHandle!");
}
}
// Perform any specific actions to release the handle in the ReleaseHandle method.
// Often, you need to use Pinvoke to make a call into the Win32 API to release the
// handle. In this case, however, we can use the Marshal class to release the unmanaged memory.
override protected bool ReleaseHandle()
{
// If the handle was set, free it. Return success.
if (!IsInvalid)
{
// NOTE: We should also ZERO out the memory allocated to the handle, before free'ing it
// so there are no traces of the sensitive data left in memory.
CredFree(handle);
// Mark the handle as invalid for future users.
SetHandleAsInvalid();
return true;
}
// Return false.
return false;
}
}
}
}

View File

@@ -0,0 +1,14 @@
//
// Code originally from http://credentialmanagement.codeplex.com/,
// Licensed under the Apache License 2.0
//
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Win32
{
public enum PersistanceType : uint
{
Session = 1,
LocalComputer = 2,
Enterprise = 3
}
}

View File

@@ -0,0 +1,290 @@
//
// Code originally from http://credentialmanagement.codeplex.com/,
// Licensed under the Apache License 2.0
//
using System;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Win32
{
public class Win32Credential: IDisposable
{
bool disposed;
CredentialType type;
string target;
SecureString password;
string username;
string description;
DateTime lastWriteTime;
PersistanceType persistanceType;
public Win32Credential()
: this(null)
{
}
public Win32Credential(string username)
: this(username, null)
{
}
public Win32Credential(string username, string password)
: this(username, password, null)
{
}
public Win32Credential(string username, string password, string target)
: this(username, password, target, CredentialType.Generic)
{
}
public Win32Credential(string username, string password, string target, CredentialType type)
{
Username = username;
Password = password;
Target = target;
Type = type;
PersistanceType = PersistanceType.Session;
lastWriteTime = DateTime.MinValue;
}
public void Dispose()
{
Dispose(true);
// Prevent GC Collection since we have already disposed of this object
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
SecurePassword.Clear();
SecurePassword.Dispose();
}
}
disposed = true;
}
private void CheckNotDisposed()
{
if (disposed)
{
throw new ObjectDisposedException(CredentialResources.CredentialDisposed);
}
}
public string Username {
get
{
CheckNotDisposed();
return username;
}
set
{
CheckNotDisposed();
username = value;
}
}
public string Password
{
get
{
return SecureStringHelper.CreateString(SecurePassword);
}
set
{
CheckNotDisposed();
SecurePassword = SecureStringHelper.CreateSecureString(string.IsNullOrEmpty(value) ? string.Empty : value);
}
}
public SecureString SecurePassword
{
get
{
CheckNotDisposed();
return null == password ? new SecureString() : password.Copy();
}
set
{
CheckNotDisposed();
if (null != password)
{
password.Clear();
password.Dispose();
}
password = null == value ? new SecureString() : value.Copy();
}
}
public string Target
{
get
{
CheckNotDisposed();
return target;
}
set
{
CheckNotDisposed();
target = value;
}
}
public string Description
{
get
{
CheckNotDisposed();
return description;
}
set
{
CheckNotDisposed();
description = value;
}
}
public DateTime LastWriteTime
{
get
{
return LastWriteTimeUtc.ToLocalTime();
}
}
public DateTime LastWriteTimeUtc
{
get
{
CheckNotDisposed();
return lastWriteTime;
}
private set { lastWriteTime = value; }
}
public CredentialType Type
{
get
{
CheckNotDisposed();
return type;
}
set
{
CheckNotDisposed();
type = value;
}
}
public PersistanceType PersistanceType
{
get
{
CheckNotDisposed();
return persistanceType;
}
set
{
CheckNotDisposed();
persistanceType = value;
}
}
public bool Save()
{
CheckNotDisposed();
byte[] passwordBytes = Encoding.Unicode.GetBytes(Password);
if (Password.Length > (512))
{
throw new ArgumentOutOfRangeException(CredentialResources.PasswordLengthExceeded);
}
NativeMethods.CREDENTIAL credential = new NativeMethods.CREDENTIAL();
credential.TargetName = Target;
credential.UserName = Username;
credential.CredentialBlob = Marshal.StringToCoTaskMemUni(Password);
credential.CredentialBlobSize = passwordBytes.Length;
credential.Comment = Description;
credential.Type = (int)Type;
credential.Persist = (int) PersistanceType;
bool result = NativeMethods.CredWrite(ref credential, 0);
if (!result)
{
return false;
}
LastWriteTimeUtc = DateTime.UtcNow;
return true;
}
public bool Delete()
{
CheckNotDisposed();
if (string.IsNullOrEmpty(Target))
{
throw new InvalidOperationException(CredentialResources.TargetRequiredForDelete);
}
StringBuilder target = string.IsNullOrEmpty(Target) ? new StringBuilder() : new StringBuilder(Target);
bool result = NativeMethods.CredDelete(target, Type, 0);
return result;
}
public bool Load()
{
CheckNotDisposed();
IntPtr credPointer;
bool result = NativeMethods.CredRead(Target, Type, 0, out credPointer);
if (!result)
{
return false;
}
using (NativeMethods.CriticalCredentialHandle credentialHandle = new NativeMethods.CriticalCredentialHandle(credPointer))
{
LoadInternal(credentialHandle.GetCredential());
}
return true;
}
public bool Exists()
{
CheckNotDisposed();
if (string.IsNullOrEmpty(Target))
{
throw new InvalidOperationException(CredentialResources.TargetRequiredForLookup);
}
using (Win32Credential existing = new Win32Credential { Target = Target, Type = Type })
{
return existing.Load();
}
}
internal void LoadInternal(NativeMethods.CREDENTIAL credential)
{
Username = credential.UserName;
if (credential.CredentialBlobSize > 0)
{
Password = Marshal.PtrToStringUni(credential.CredentialBlob, credential.CredentialBlobSize / 2);
}
Target = credential.TargetName;
Type = (CredentialType)credential.Type;
PersistanceType = (PersistanceType)credential.Persist;
Description = credential.Comment;
LastWriteTimeUtc = DateTime.FromFileTimeUtc(credential.LastWritten);
}
}
}

View File

@@ -0,0 +1,63 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.EditorServices.Utility;
using Microsoft.SqlTools.ServiceLayer.Credentials.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Win32
{
/// <summary>
/// Win32 implementation of the credential store
/// </summary>
internal class Win32CredentialStore : ICredentialStore
{
private const string AnyUsername = "*";
public bool DeletePassword(string credentialId)
{
using (Win32Credential cred = new Win32Credential() { Target = credentialId, Username = AnyUsername })
{
return cred.Delete();
}
}
public bool TryGetPassword(string credentialId, out string password)
{
Validate.IsNotNullOrEmptyString("credentialId", credentialId);
password = null;
using (CredentialSet set = new CredentialSet(credentialId).Load())
{
// Note: Credentials are disposed on disposal of the set
Win32Credential foundCred = null;
if (set.Count > 0)
{
foundCred = set[0];
}
if (foundCred != null)
{
password = foundCred.Password;
return true;
}
return false;
}
}
public bool Save(Credential credential)
{
Credential.ValidateForSave(credential);
using (Win32Credential cred =
new Win32Credential(AnyUsername, credential.Password, credential.CredentialId, CredentialType.Generic)
{ PersistanceType = PersistanceType.LocalComputer })
{
return cred.Save();
}
}
}
}