8e92cca139
* win_credential_manager: new module to manage credentials * fix sanity issues and removed CredSSP references * renamed module to win_credential * fix typo on test variable * fix sanity ignore line
498 lines
20 KiB
PowerShell
498 lines
20 KiB
PowerShell
#!powershell
|
|
|
|
# Copyright: (c) 2018, Ansible Project
|
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
|
|
#AnsibleRequires -CSharpUtil Ansible.Basic
|
|
#Requires -Module Ansible.ModuleUtils.AddType
|
|
|
|
$spec = @{
|
|
options = @{
|
|
name = @{ type = "str"; required = $true }
|
|
type = @{ type = "str"; required = $true; choices = @("domain_password", "domain_certificate", "generic_password", "generic_certificate") }
|
|
}
|
|
supports_check_mode = $true
|
|
}
|
|
|
|
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
|
|
|
$name = $module.Params.name
|
|
$type = $module.Params.type
|
|
|
|
Add-CSharpType -AnsibleModule $module -References @'
|
|
using Microsoft.Win32.SafeHandles;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Runtime.ConstrainedExecution;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
|
|
namespace Ansible.CredentialManager
|
|
{
|
|
internal class NativeHelpers
|
|
{
|
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
public class CREDENTIAL
|
|
{
|
|
public CredentialFlags Flags;
|
|
public CredentialType Type;
|
|
[MarshalAs(UnmanagedType.LPWStr)] public string TargetName;
|
|
[MarshalAs(UnmanagedType.LPWStr)] public string Comment;
|
|
public FILETIME LastWritten;
|
|
public UInt32 CredentialBlobSize;
|
|
public IntPtr CredentialBlob;
|
|
public CredentialPersist Persist;
|
|
public UInt32 AttributeCount;
|
|
public IntPtr Attributes;
|
|
[MarshalAs(UnmanagedType.LPWStr)] public string TargetAlias;
|
|
[MarshalAs(UnmanagedType.LPWStr)] public string UserName;
|
|
|
|
public static explicit operator Credential(CREDENTIAL v)
|
|
{
|
|
byte[] secret = new byte[(int)v.CredentialBlobSize];
|
|
if (v.CredentialBlob != IntPtr.Zero)
|
|
Marshal.Copy(v.CredentialBlob, secret, 0, secret.Length);
|
|
|
|
List<CredentialAttribute> attributes = new List<CredentialAttribute>();
|
|
if (v.AttributeCount > 0)
|
|
{
|
|
CREDENTIAL_ATTRIBUTE[] rawAttributes = new CREDENTIAL_ATTRIBUTE[v.AttributeCount];
|
|
Credential.PtrToStructureArray(rawAttributes, v.Attributes);
|
|
attributes = rawAttributes.Select(x => (CredentialAttribute)x).ToList();
|
|
}
|
|
|
|
string userName = v.UserName;
|
|
if (v.Type == CredentialType.DomainCertificate || v.Type == CredentialType.GenericCertificate)
|
|
userName = Credential.UnmarshalCertificateCredential(userName);
|
|
|
|
return new Credential
|
|
{
|
|
Type = v.Type,
|
|
TargetName = v.TargetName,
|
|
Comment = v.Comment,
|
|
LastWritten = (DateTimeOffset)v.LastWritten,
|
|
Secret = secret,
|
|
Persist = v.Persist,
|
|
Attributes = attributes,
|
|
TargetAlias = v.TargetAlias,
|
|
UserName = userName,
|
|
Loaded = true,
|
|
};
|
|
}
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct CREDENTIAL_ATTRIBUTE
|
|
{
|
|
[MarshalAs(UnmanagedType.LPWStr)] public string Keyword;
|
|
public UInt32 Flags; // Set to 0 and is reserved
|
|
public UInt32 ValueSize;
|
|
public IntPtr Value;
|
|
|
|
public static explicit operator CredentialAttribute(CREDENTIAL_ATTRIBUTE v)
|
|
{
|
|
byte[] value = new byte[v.ValueSize];
|
|
Marshal.Copy(v.Value, value, 0, (int)v.ValueSize);
|
|
|
|
return new CredentialAttribute
|
|
{
|
|
Keyword = v.Keyword,
|
|
Flags = v.Flags,
|
|
Value = value,
|
|
};
|
|
}
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct FILETIME
|
|
{
|
|
internal UInt32 dwLowDateTime;
|
|
internal UInt32 dwHighDateTime;
|
|
|
|
public static implicit operator long(FILETIME v) { return ((long)v.dwHighDateTime << 32) + v.dwLowDateTime; }
|
|
public static explicit operator DateTimeOffset(FILETIME v) { return DateTimeOffset.FromFileTime(v); }
|
|
public static explicit operator FILETIME(DateTimeOffset v)
|
|
{
|
|
return new FILETIME()
|
|
{
|
|
dwLowDateTime = (UInt32)v.ToFileTime(),
|
|
dwHighDateTime = ((UInt32)v.ToFileTime() >> 32),
|
|
};
|
|
}
|
|
}
|
|
|
|
[Flags]
|
|
public enum CredentialCreateFlags : uint
|
|
{
|
|
PreserveCredentialBlob = 1,
|
|
}
|
|
|
|
[Flags]
|
|
public enum CredentialFlags
|
|
{
|
|
None = 0,
|
|
PromptNow = 2,
|
|
UsernameTarget = 4,
|
|
}
|
|
|
|
public enum CredMarshalType : uint
|
|
{
|
|
CertCredential = 1,
|
|
UsernameTargetCredential,
|
|
BinaryBlobCredential,
|
|
UsernameForPackedCredential,
|
|
BinaryBlobForSystem,
|
|
}
|
|
}
|
|
|
|
internal class NativeMethods
|
|
{
|
|
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
public static extern bool CredDeleteW(
|
|
[MarshalAs(UnmanagedType.LPWStr)] string TargetName,
|
|
CredentialType Type,
|
|
UInt32 Flags);
|
|
|
|
[DllImport("advapi32.dll")]
|
|
public static extern void CredFree(
|
|
IntPtr Buffer);
|
|
|
|
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
public static extern bool CredMarshalCredentialW(
|
|
NativeHelpers.CredMarshalType CredType,
|
|
SafeMemoryBuffer Credential,
|
|
out SafeCredentialBuffer MarshaledCredential);
|
|
|
|
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
public static extern bool CredReadW(
|
|
[MarshalAs(UnmanagedType.LPWStr)] string TargetName,
|
|
CredentialType Type,
|
|
UInt32 Flags,
|
|
out SafeCredentialBuffer Credential);
|
|
|
|
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
public static extern bool CredUnmarshalCredentialW(
|
|
[MarshalAs(UnmanagedType.LPWStr)] string MarshaledCredential,
|
|
out NativeHelpers.CredMarshalType CredType,
|
|
out SafeCredentialBuffer Credential);
|
|
|
|
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
public static extern bool CredWriteW(
|
|
NativeHelpers.CREDENTIAL Credential,
|
|
NativeHelpers.CredentialCreateFlags Flags);
|
|
}
|
|
|
|
internal class SafeCredentialBuffer : SafeHandleZeroOrMinusOneIsInvalid
|
|
{
|
|
public SafeCredentialBuffer() : base(true) { }
|
|
|
|
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
|
|
protected override bool ReleaseHandle()
|
|
{
|
|
NativeMethods.CredFree(handle);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid
|
|
{
|
|
public SafeMemoryBuffer() : base(true) { }
|
|
public SafeMemoryBuffer(int cb) : base(true)
|
|
{
|
|
base.SetHandle(Marshal.AllocHGlobal(cb));
|
|
}
|
|
public SafeMemoryBuffer(IntPtr handle) : base(true)
|
|
{
|
|
base.SetHandle(handle);
|
|
}
|
|
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
|
|
protected override bool ReleaseHandle()
|
|
{
|
|
Marshal.FreeHGlobal(handle);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public class Win32Exception : System.ComponentModel.Win32Exception
|
|
{
|
|
private string _exception_msg;
|
|
public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
|
|
public Win32Exception(int errorCode, string message) : base(errorCode)
|
|
{
|
|
_exception_msg = String.Format("{0} - {1} (Win32 Error Code {2}: 0x{3})", message, base.Message, errorCode, errorCode.ToString("X8"));
|
|
}
|
|
public override string Message { get { return _exception_msg; } }
|
|
public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
|
|
}
|
|
|
|
public enum CredentialPersist
|
|
{
|
|
Session = 1,
|
|
LocalMachine = 2,
|
|
Enterprise = 3,
|
|
}
|
|
|
|
public enum CredentialType
|
|
{
|
|
Generic = 1,
|
|
DomainPassword = 2,
|
|
DomainCertificate = 3,
|
|
DomainVisiblePassword = 4,
|
|
GenericCertificate = 5,
|
|
DomainExtended = 6,
|
|
Maximum = 7,
|
|
MaximumEx = 1007,
|
|
}
|
|
|
|
public class CredentialAttribute
|
|
{
|
|
public string Keyword;
|
|
public UInt32 Flags;
|
|
public byte[] Value;
|
|
}
|
|
|
|
public class Credential
|
|
{
|
|
public CredentialType Type;
|
|
public string TargetName;
|
|
public string Comment;
|
|
public DateTimeOffset LastWritten;
|
|
public byte[] Secret;
|
|
public CredentialPersist Persist;
|
|
public List<CredentialAttribute> Attributes = new List<CredentialAttribute>();
|
|
public string TargetAlias;
|
|
public string UserName;
|
|
|
|
// Used to track whether the credential has been loaded into the store or not
|
|
public bool Loaded { get; internal set; }
|
|
|
|
public void Delete()
|
|
{
|
|
if (!Loaded)
|
|
return;
|
|
|
|
if (!NativeMethods.CredDeleteW(TargetName, Type, 0))
|
|
throw new Win32Exception(String.Format("CredDeleteW({0}) failed", TargetName));
|
|
Loaded = false;
|
|
}
|
|
|
|
public void Write(bool preserveExisting)
|
|
{
|
|
string userName = UserName;
|
|
// Convert the certificate thumbprint to the string expected
|
|
if (Type == CredentialType.DomainCertificate || Type == CredentialType.GenericCertificate)
|
|
userName = Credential.MarshalCertificateCredential(userName);
|
|
|
|
NativeHelpers.CREDENTIAL credential = new NativeHelpers.CREDENTIAL
|
|
{
|
|
Flags = NativeHelpers.CredentialFlags.None,
|
|
Type = Type,
|
|
TargetName = TargetName,
|
|
Comment = Comment,
|
|
LastWritten = new NativeHelpers.FILETIME(),
|
|
CredentialBlobSize = (UInt32)(Secret == null ? 0 : Secret.Length),
|
|
CredentialBlob = IntPtr.Zero, // Must be allocated and freed outside of this to ensure no memory leaks
|
|
Persist = Persist,
|
|
AttributeCount = (UInt32)(Attributes.Count),
|
|
Attributes = IntPtr.Zero, // Attributes must be allocated and freed outside of this to ensure no memory leaks
|
|
TargetAlias = TargetAlias,
|
|
UserName = userName,
|
|
};
|
|
|
|
using (SafeMemoryBuffer credentialBlob = new SafeMemoryBuffer((int)credential.CredentialBlobSize))
|
|
{
|
|
if (Secret != null)
|
|
Marshal.Copy(Secret, 0, credentialBlob.DangerousGetHandle(), Secret.Length);
|
|
credential.CredentialBlob = credentialBlob.DangerousGetHandle();
|
|
|
|
// Store the CREDENTIAL_ATTRIBUTE value in a safe memory buffer and make sure we dispose in all cases
|
|
List<SafeMemoryBuffer> attributeBuffers = new List<SafeMemoryBuffer>();
|
|
try
|
|
{
|
|
int attributeLength = Attributes.Sum(a => Marshal.SizeOf(typeof(NativeHelpers.CREDENTIAL_ATTRIBUTE)));
|
|
byte[] attributeBytes = new byte[attributeLength];
|
|
int offset = 0;
|
|
foreach (CredentialAttribute attribute in Attributes)
|
|
{
|
|
SafeMemoryBuffer attributeBuffer = new SafeMemoryBuffer(attribute.Value.Length);
|
|
attributeBuffers.Add(attributeBuffer);
|
|
if (attribute.Value != null)
|
|
Marshal.Copy(attribute.Value, 0, attributeBuffer.DangerousGetHandle(), attribute.Value.Length);
|
|
|
|
NativeHelpers.CREDENTIAL_ATTRIBUTE credentialAttribute = new NativeHelpers.CREDENTIAL_ATTRIBUTE
|
|
{
|
|
Keyword = attribute.Keyword,
|
|
Flags = attribute.Flags,
|
|
ValueSize = (UInt32)(attribute.Value == null ? 0 : attribute.Value.Length),
|
|
Value = attributeBuffer.DangerousGetHandle(),
|
|
};
|
|
int attributeStructLength = Marshal.SizeOf(typeof(NativeHelpers.CREDENTIAL_ATTRIBUTE));
|
|
|
|
byte[] attrBytes = new byte[attributeStructLength];
|
|
using (SafeMemoryBuffer tempBuffer = new SafeMemoryBuffer(attributeStructLength))
|
|
{
|
|
Marshal.StructureToPtr(credentialAttribute, tempBuffer.DangerousGetHandle(), false);
|
|
Marshal.Copy(tempBuffer.DangerousGetHandle(), attrBytes, 0, attributeStructLength);
|
|
}
|
|
Buffer.BlockCopy(attrBytes, 0, attributeBytes, offset, attributeStructLength);
|
|
offset += attributeStructLength;
|
|
}
|
|
|
|
using (SafeMemoryBuffer attributes = new SafeMemoryBuffer(attributeBytes.Length))
|
|
{
|
|
if (attributeBytes.Length != 0)
|
|
Marshal.Copy(attributeBytes, 0, attributes.DangerousGetHandle(), attributeBytes.Length);
|
|
credential.Attributes = attributes.DangerousGetHandle();
|
|
|
|
NativeHelpers.CredentialCreateFlags createFlags = 0;
|
|
if (preserveExisting)
|
|
createFlags |= NativeHelpers.CredentialCreateFlags.PreserveCredentialBlob;
|
|
|
|
if (!NativeMethods.CredWriteW(credential, createFlags))
|
|
throw new Win32Exception(String.Format("CredWriteW({0}) failed", TargetName));
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
foreach (SafeMemoryBuffer attributeBuffer in attributeBuffers)
|
|
attributeBuffer.Dispose();
|
|
}
|
|
}
|
|
Loaded = true;
|
|
}
|
|
|
|
public static Credential GetCredential(string target, CredentialType type)
|
|
{
|
|
SafeCredentialBuffer buffer;
|
|
if (!NativeMethods.CredReadW(target, type, 0, out buffer))
|
|
{
|
|
int lastErr = Marshal.GetLastWin32Error();
|
|
|
|
// Not running with CredSSP or Become so cannot manage the user's credentials
|
|
if (lastErr == 0x00000520) // ERROR_NO_SUCH_LOGON_SESSION
|
|
throw new InvalidOperationException("Failed to access the user's credential store, run the module with become or CredSSP");
|
|
else if (lastErr == 0x00000490) // ERROR_NOT_FOUND
|
|
return null;
|
|
throw new Win32Exception(lastErr, "CredEnumerateW() failed");
|
|
}
|
|
|
|
using (buffer)
|
|
{
|
|
NativeHelpers.CREDENTIAL credential = (NativeHelpers.CREDENTIAL)Marshal.PtrToStructure(
|
|
buffer.DangerousGetHandle(), typeof(NativeHelpers.CREDENTIAL));
|
|
return (Credential)credential;
|
|
}
|
|
}
|
|
|
|
public static string MarshalCertificateCredential(string thumbprint)
|
|
{
|
|
// CredWriteW requires the UserName field to be the value of CredMarshalCredentialW() when writting a
|
|
// certificate auth. This converts the UserName property to the format required.
|
|
|
|
// While CERT_CREDENTIAL_INFO is the correct structure, we manually marshal the data in order to
|
|
// support different cert hash lengths in the future.
|
|
// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_cert_credential_info
|
|
int hexLength = thumbprint.Length;
|
|
byte[] credInfo = new byte[sizeof(UInt32) + (hexLength / 2)];
|
|
|
|
// First field is cbSize which is a UInt32 value denoting the size of the total structure
|
|
Array.Copy(BitConverter.GetBytes((UInt32)credInfo.Length), credInfo, sizeof(UInt32));
|
|
|
|
// Now copy the byte representation of the thumbprint to the rest of the struct bytes
|
|
for (int i = 0; i < hexLength; i += 2)
|
|
credInfo[sizeof(UInt32) + (i / 2)] = Convert.ToByte(thumbprint.Substring(i, 2), 16);
|
|
|
|
IntPtr pCredInfo = Marshal.AllocHGlobal(credInfo.Length);
|
|
Marshal.Copy(credInfo, 0, pCredInfo, credInfo.Length);
|
|
SafeMemoryBuffer pCredential = new SafeMemoryBuffer(pCredInfo);
|
|
|
|
NativeHelpers.CredMarshalType marshalType = NativeHelpers.CredMarshalType.CertCredential;
|
|
using (pCredential)
|
|
{
|
|
SafeCredentialBuffer marshaledCredential;
|
|
if (!NativeMethods.CredMarshalCredentialW(marshalType, pCredential, out marshaledCredential))
|
|
throw new Win32Exception("CredMarshalCredentialW() failed");
|
|
using (marshaledCredential)
|
|
return Marshal.PtrToStringUni(marshaledCredential.DangerousGetHandle());
|
|
}
|
|
}
|
|
|
|
public static string UnmarshalCertificateCredential(string value)
|
|
{
|
|
NativeHelpers.CredMarshalType credType;
|
|
SafeCredentialBuffer pCredInfo;
|
|
if (!NativeMethods.CredUnmarshalCredentialW(value, out credType, out pCredInfo))
|
|
throw new Win32Exception("CredUnmarshalCredentialW() failed");
|
|
|
|
using (pCredInfo)
|
|
{
|
|
if (credType != NativeHelpers.CredMarshalType.CertCredential)
|
|
throw new InvalidOperationException(String.Format("Expected unmarshalled cred type of CertCredential, received {0}", credType));
|
|
|
|
byte[] structSizeBytes = new byte[sizeof(UInt32)];
|
|
Marshal.Copy(pCredInfo.DangerousGetHandle(), structSizeBytes, 0, sizeof(UInt32));
|
|
UInt32 structSize = BitConverter.ToUInt32(structSizeBytes, 0);
|
|
|
|
byte[] certInfoBytes = new byte[structSize];
|
|
Marshal.Copy(pCredInfo.DangerousGetHandle(), certInfoBytes, 0, certInfoBytes.Length);
|
|
|
|
StringBuilder hex = new StringBuilder((certInfoBytes.Length - sizeof(UInt32)) * 2);
|
|
for (int i = 4; i < certInfoBytes.Length; i++)
|
|
hex.AppendFormat("{0:x2}", certInfoBytes[i]);
|
|
|
|
return hex.ToString().ToUpperInvariant();
|
|
}
|
|
}
|
|
|
|
internal static void PtrToStructureArray<T>(T[] array, IntPtr ptr)
|
|
{
|
|
IntPtr ptrOffset = ptr;
|
|
for (int i = 0; i < array.Length; i++, ptrOffset = IntPtr.Add(ptrOffset, Marshal.SizeOf(typeof(T))))
|
|
array[i] = (T)Marshal.PtrToStructure(ptrOffset, typeof(T));
|
|
}
|
|
}
|
|
}
|
|
'@
|
|
|
|
$type = switch ($type) {
|
|
"domain_password" { [Ansible.CredentialManager.CredentialType]::DomainPassword }
|
|
"domain_certificate" { [Ansible.CredentialManager.CredentialType]::DomainCertificate }
|
|
"generic_password" { [Ansible.CredentialManager.CredentialType]::Generic }
|
|
"generic_certificate" { [Ansible.CredentialManager.CredentialType]::GenericCertificate }
|
|
}
|
|
|
|
$credential = [Ansible.CredentialManager.Credential]::GetCredential($name, $type)
|
|
if ($null -ne $credential) {
|
|
$module.Result.exists = $true
|
|
$module.Result.alias = $credential.TargetAlias
|
|
$module.Result.attributes = [System.Collections.ArrayList]@()
|
|
$module.Result.comment = $credential.Comment
|
|
$module.Result.name = $credential.TargetName
|
|
$module.Result.persistence = $credential.Persist.ToString()
|
|
$module.Result.type = $credential.Type.ToString()
|
|
$module.Result.username = $credential.UserName
|
|
|
|
if ($null -ne $credential.Secret) {
|
|
$module.Result.secret = [System.Convert]::ToBase64String($credential.Secret)
|
|
} else {
|
|
$module.Result.secret = $null
|
|
}
|
|
|
|
foreach ($attribute in $credential.Attributes) {
|
|
$attribute_info = @{
|
|
name = $attribute.Keyword
|
|
}
|
|
if ($null -ne $attribute.Value) {
|
|
$attribute_info.data = [System.Convert]::ToBase64String($attribute.Value)
|
|
} else {
|
|
$attribute_info.data = $null
|
|
}
|
|
$module.Result.attributes.Add($attribute_info) > $null
|
|
}
|
|
} else {
|
|
$module.Result.exists = $false
|
|
}
|
|
|
|
$module.ExitJson()
|
|
|