1 #!powershell
2 
3 # Copyright: (c) 2018, Ansible Project
4 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5 
6 #AnsibleRequires -CSharpUtil Ansible.Basic
7 #Requires -Module Ansible.ModuleUtils.AddType
8 
9 $spec = @{
10     options = @{
11         name = @{ type = "str"; required = $true }
12         type = @{ type = "str"; required = $true; choices = @("domain_password", "domain_certificate", "generic_password", "generic_certificate") }
13     }
14     supports_check_mode = $true
15 }
16 
17 $module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
18 
19 $name = $module.Params.name
20 $type = $module.Params.type
21 
22 Add-CSharpType -AnsibleModule $module -References @'
23 using Microsoft.Win32.SafeHandles;
24 using System;
25 using System.Collections.Generic;
26 using System.Linq;
27 using System.Runtime.ConstrainedExecution;
28 using System.Runtime.InteropServices;
29 using System.Text;
30 
31 namespace Ansible.CredentialManager
32 {
33     internal class NativeHelpers
34     {
35         [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
36         public class CREDENTIAL
37         {
38             public CredentialFlags Flags;
39             public CredentialType Type;
40             [MarshalAs(UnmanagedType.LPWStr)] public string TargetName;
41             [MarshalAs(UnmanagedType.LPWStr)] public string Comment;
42             public FILETIME LastWritten;
43             public UInt32 CredentialBlobSize;
44             public IntPtr CredentialBlob;
45             public CredentialPersist Persist;
46             public UInt32 AttributeCount;
47             public IntPtr Attributes;
48             [MarshalAs(UnmanagedType.LPWStr)] public string TargetAlias;
49             [MarshalAs(UnmanagedType.LPWStr)] public string UserName;
50 
51             public static explicit operator Credential(CREDENTIAL v)
52             {
53                 byte[] secret = new byte[(int)v.CredentialBlobSize];
54                 if (v.CredentialBlob != IntPtr.Zero)
55                     Marshal.Copy(v.CredentialBlob, secret, 0, secret.Length);
56 
57                 List<CredentialAttribute> attributes = new List<CredentialAttribute>();
58                 if (v.AttributeCount > 0)
59                 {
60                     CREDENTIAL_ATTRIBUTE[] rawAttributes = new CREDENTIAL_ATTRIBUTE[v.AttributeCount];
61                     Credential.PtrToStructureArray(rawAttributes, v.Attributes);
62                     attributes = rawAttributes.Select(x => (CredentialAttribute)x).ToList();
63                 }
64 
65                 string userName = v.UserName;
66                 if (v.Type == CredentialType.DomainCertificate || v.Type == CredentialType.GenericCertificate)
67                     userName = Credential.UnmarshalCertificateCredential(userName);
68 
69                 return new Credential
70                 {
71                     Type = v.Type,
72                     TargetName = v.TargetName,
73                     Comment = v.Comment,
74                     LastWritten = (DateTimeOffset)v.LastWritten,
75                     Secret = secret,
76                     Persist = v.Persist,
77                     Attributes = attributes,
78                     TargetAlias = v.TargetAlias,
79                     UserName = userName,
80                     Loaded = true,
81                 };
82             }
83         }
84 
85         [StructLayout(LayoutKind.Sequential)]
86         public struct CREDENTIAL_ATTRIBUTE
87         {
88             [MarshalAs(UnmanagedType.LPWStr)] public string Keyword;
89             public UInt32 Flags;  // Set to 0 and is reserved
90             public UInt32 ValueSize;
91             public IntPtr Value;
92 
93             public static explicit operator CredentialAttribute(CREDENTIAL_ATTRIBUTE v)
94             {
95                 byte[] value = new byte[v.ValueSize];
96                 Marshal.Copy(v.Value, value, 0, (int)v.ValueSize);
97 
98                 return new CredentialAttribute
99                 {
100                     Keyword = v.Keyword,
101                     Flags = v.Flags,
102                     Value = value,
103                 };
104             }
105         }
106 
107         [StructLayout(LayoutKind.Sequential)]
108         public struct FILETIME
109         {
110             internal UInt32 dwLowDateTime;
111             internal UInt32 dwHighDateTime;
112 
113             public static implicit operator long(FILETIME v) { return ((long)v.dwHighDateTime << 32) + v.dwLowDateTime; }
114             public static explicit operator DateTimeOffset(FILETIME v) { return DateTimeOffset.FromFileTime(v); }
115             public static explicit operator FILETIME(DateTimeOffset v)
116             {
117                 return new FILETIME()
118                 {
119                     dwLowDateTime = (UInt32)v.ToFileTime(),
120                     dwHighDateTime = ((UInt32)v.ToFileTime() >> 32),
121                 };
122             }
123         }
124 
125         [Flags]
126         public enum CredentialCreateFlags : uint
127         {
128             PreserveCredentialBlob = 1,
129         }
130 
131         [Flags]
132         public enum CredentialFlags
133         {
134             None = 0,
135             PromptNow = 2,
136             UsernameTarget = 4,
137         }
138 
139         public enum CredMarshalType : uint
140         {
141             CertCredential = 1,
142             UsernameTargetCredential,
143             BinaryBlobCredential,
144             UsernameForPackedCredential,
145             BinaryBlobForSystem,
146         }
147     }
148 
149     internal class NativeMethods
150     {
151         [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
152         public static extern bool CredDeleteW(
153             [MarshalAs(UnmanagedType.LPWStr)] string TargetName,
154             CredentialType Type,
155             UInt32 Flags);
156 
157         [DllImport("advapi32.dll")]
158         public static extern void CredFree(
159             IntPtr Buffer);
160 
161         [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
162         public static extern bool CredMarshalCredentialW(
163             NativeHelpers.CredMarshalType CredType,
164             SafeMemoryBuffer Credential,
165             out SafeCredentialBuffer MarshaledCredential);
166 
167         [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
168         public static extern bool CredReadW(
169             [MarshalAs(UnmanagedType.LPWStr)] string TargetName,
170             CredentialType Type,
171             UInt32 Flags,
172             out SafeCredentialBuffer Credential);
173 
174         [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
175         public static extern bool CredUnmarshalCredentialW(
176             [MarshalAs(UnmanagedType.LPWStr)] string MarshaledCredential,
177             out NativeHelpers.CredMarshalType CredType,
178             out SafeCredentialBuffer Credential);
179 
180         [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
181         public static extern bool CredWriteW(
182             NativeHelpers.CREDENTIAL Credential,
183             NativeHelpers.CredentialCreateFlags Flags);
184     }
185 
186     internal class SafeCredentialBuffer : SafeHandleZeroOrMinusOneIsInvalid
187     {
188         public SafeCredentialBuffer() : base(true) { }
189 
190         [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
191         protected override bool ReleaseHandle()
192         {
193             NativeMethods.CredFree(handle);
194             return true;
195         }
196     }
197 
198     internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid
199     {
200         public SafeMemoryBuffer() : base(true) { }
201         public SafeMemoryBuffer(int cb) : base(true)
202         {
203             base.SetHandle(Marshal.AllocHGlobal(cb));
204         }
205         public SafeMemoryBuffer(IntPtr handle) : base(true)
206         {
207             base.SetHandle(handle);
208         }
209         [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
210         protected override bool ReleaseHandle()
211         {
212             Marshal.FreeHGlobal(handle);
213             return true;
214         }
215     }
216 
217     public class Win32Exception : System.ComponentModel.Win32Exception
218     {
219         private string _exception_msg;
220         public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
221         public Win32Exception(int errorCode, string message) : base(errorCode)
222         {
223             _exception_msg = String.Format("{0} - {1} (Win32 Error Code {2}: 0x{3})", message, base.Message, errorCode, errorCode.ToString("X8"));
224         }
225         public override string Message { get { return _exception_msg; } }
226         public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
227     }
228 
229     public enum CredentialPersist
230     {
231         Session = 1,
232         LocalMachine = 2,
233         Enterprise = 3,
234     }
235 
236     public enum CredentialType
237     {
238         Generic = 1,
239         DomainPassword = 2,
240         DomainCertificate = 3,
241         DomainVisiblePassword = 4,
242         GenericCertificate = 5,
243         DomainExtended = 6,
244         Maximum = 7,
245         MaximumEx = 1007,
246     }
247 
248     public class CredentialAttribute
249     {
250         public string Keyword;
251         public UInt32 Flags;
252         public byte[] Value;
253     }
254 
255     public class Credential
256     {
257         public CredentialType Type;
258         public string TargetName;
259         public string Comment;
260         public DateTimeOffset LastWritten;
261         public byte[] Secret;
262         public CredentialPersist Persist;
263         public List<CredentialAttribute> Attributes = new List<CredentialAttribute>();
264         public string TargetAlias;
265         public string UserName;
266 
267         // Used to track whether the credential has been loaded into the store or not
268         public bool Loaded { get; internal set; }
269 
270         public void Delete()
271         {
272             if (!Loaded)
273                 return;
274 
275             if (!NativeMethods.CredDeleteW(TargetName, Type, 0))
276                 throw new Win32Exception(String.Format("CredDeleteW({0}) failed", TargetName));
277             Loaded = false;
278         }
279 
280         public void Write(bool preserveExisting)
281         {
282             string userName = UserName;
283             // Convert the certificate thumbprint to the string expected
284             if (Type == CredentialType.DomainCertificate || Type == CredentialType.GenericCertificate)
285                 userName = Credential.MarshalCertificateCredential(userName);
286 
287             NativeHelpers.CREDENTIAL credential = new NativeHelpers.CREDENTIAL
288             {
289                 Flags = NativeHelpers.CredentialFlags.None,
290                 Type = Type,
291                 TargetName = TargetName,
292                 Comment = Comment,
293                 LastWritten = new NativeHelpers.FILETIME(),
294                 CredentialBlobSize = (UInt32)(Secret == null ? 0 : Secret.Length),
295                 CredentialBlob = IntPtr.Zero, // Must be allocated and freed outside of this to ensure no memory leaks
296                 Persist = Persist,
297                 AttributeCount = (UInt32)(Attributes.Count),
298                 Attributes = IntPtr.Zero, // Attributes must be allocated and freed outside of this to ensure no memory leaks
299                 TargetAlias = TargetAlias,
300                 UserName = userName,
301             };
302 
303             using (SafeMemoryBuffer credentialBlob = new SafeMemoryBuffer((int)credential.CredentialBlobSize))
304             {
305                 if (Secret != null)
306                     Marshal.Copy(Secret, 0, credentialBlob.DangerousGetHandle(), Secret.Length);
307                 credential.CredentialBlob = credentialBlob.DangerousGetHandle();
308 
309                 // Store the CREDENTIAL_ATTRIBUTE value in a safe memory buffer and make sure we dispose in all cases
310                 List<SafeMemoryBuffer> attributeBuffers = new List<SafeMemoryBuffer>();
311                 try
312                 {
313                     int attributeLength = Attributes.Sum(a => Marshal.SizeOf(typeof(NativeHelpers.CREDENTIAL_ATTRIBUTE)));
314                     byte[] attributeBytes = new byte[attributeLength];
315                     int offset = 0;
316                     foreach (CredentialAttribute attribute in Attributes)
317                     {
318                         SafeMemoryBuffer attributeBuffer = new SafeMemoryBuffer(attribute.Value.Length);
319                         attributeBuffers.Add(attributeBuffer);
320                         if (attribute.Value != null)
321                             Marshal.Copy(attribute.Value, 0, attributeBuffer.DangerousGetHandle(), attribute.Value.Length);
322 
323                         NativeHelpers.CREDENTIAL_ATTRIBUTE credentialAttribute = new NativeHelpers.CREDENTIAL_ATTRIBUTE
324                         {
325                             Keyword = attribute.Keyword,
326                             Flags = attribute.Flags,
327                             ValueSize = (UInt32)(attribute.Value == null ? 0 : attribute.Value.Length),
328                             Value = attributeBuffer.DangerousGetHandle(),
329                         };
330                         int attributeStructLength = Marshal.SizeOf(typeof(NativeHelpers.CREDENTIAL_ATTRIBUTE));
331 
332                         byte[] attrBytes = new byte[attributeStructLength];
333                         using (SafeMemoryBuffer tempBuffer = new SafeMemoryBuffer(attributeStructLength))
334                         {
335                             Marshal.StructureToPtr(credentialAttribute, tempBuffer.DangerousGetHandle(), false);
336                             Marshal.Copy(tempBuffer.DangerousGetHandle(), attrBytes, 0, attributeStructLength);
337                         }
338                         Buffer.BlockCopy(attrBytes, 0, attributeBytes, offset, attributeStructLength);
339                         offset += attributeStructLength;
340                     }
341 
342                     using (SafeMemoryBuffer attributes = new SafeMemoryBuffer(attributeBytes.Length))
343                     {
344                         if (attributeBytes.Length != 0)
345                             Marshal.Copy(attributeBytes, 0, attributes.DangerousGetHandle(), attributeBytes.Length);
346                         credential.Attributes = attributes.DangerousGetHandle();
347 
348                         NativeHelpers.CredentialCreateFlags createFlags = 0;
349                         if (preserveExisting)
350                             createFlags |= NativeHelpers.CredentialCreateFlags.PreserveCredentialBlob;
351 
352                         if (!NativeMethods.CredWriteW(credential, createFlags))
353                             throw new Win32Exception(String.Format("CredWriteW({0}) failed", TargetName));
354                     }
355                 }
356                 finally
357                 {
358                     foreach (SafeMemoryBuffer attributeBuffer in attributeBuffers)
359                         attributeBuffer.Dispose();
360                 }
361             }
362             Loaded = true;
363         }
364 
365         public static Credential GetCredential(string target, CredentialType type)
366         {
367             SafeCredentialBuffer buffer;
368             if (!NativeMethods.CredReadW(target, type, 0, out buffer))
369             {
370                 int lastErr = Marshal.GetLastWin32Error();
371 
372                 // Not running with CredSSP or Become so cannot manage the user's credentials
373                 if (lastErr == 0x00000520) // ERROR_NO_SUCH_LOGON_SESSION
374                     throw new InvalidOperationException("Failed to access the user's credential store, run the module with become or CredSSP");
375                 else if (lastErr == 0x00000490)  // ERROR_NOT_FOUND
376                     return null;
377                 throw new Win32Exception(lastErr, "CredEnumerateW() failed");
378             }
379 
380             using (buffer)
381             {
382                 NativeHelpers.CREDENTIAL credential = (NativeHelpers.CREDENTIAL)Marshal.PtrToStructure(
383                     buffer.DangerousGetHandle(), typeof(NativeHelpers.CREDENTIAL));
384                 return (Credential)credential;
385             }
386         }
387 
388         public static string MarshalCertificateCredential(string thumbprint)
389         {
390             // CredWriteW requires the UserName field to be the value of CredMarshalCredentialW() when writting a
391             // certificate auth. This converts the UserName property to the format required.
392 
393             // While CERT_CREDENTIAL_INFO is the correct structure, we manually marshal the data in order to
394             // support different cert hash lengths in the future.
395             // https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_cert_credential_info
396             int hexLength = thumbprint.Length;
397             byte[] credInfo = new byte[sizeof(UInt32) + (hexLength / 2)];
398 
399             // First field is cbSize which is a UInt32 value denoting the size of the total structure
400             Array.Copy(BitConverter.GetBytes((UInt32)credInfo.Length), credInfo, sizeof(UInt32));
401 
402             // Now copy the byte representation of the thumbprint to the rest of the struct bytes
403             for (int i = 0; i < hexLength; i += 2)
404                 credInfo[sizeof(UInt32) + (i / 2)] = Convert.ToByte(thumbprint.Substring(i, 2), 16);
405 
406             IntPtr pCredInfo = Marshal.AllocHGlobal(credInfo.Length);
407             Marshal.Copy(credInfo, 0, pCredInfo, credInfo.Length);
408             SafeMemoryBuffer pCredential = new SafeMemoryBuffer(pCredInfo);
409 
410             NativeHelpers.CredMarshalType marshalType = NativeHelpers.CredMarshalType.CertCredential;
411             using (pCredential)
412             {
413                 SafeCredentialBuffer marshaledCredential;
414                 if (!NativeMethods.CredMarshalCredentialW(marshalType, pCredential, out marshaledCredential))
415                     throw new Win32Exception("CredMarshalCredentialW() failed");
416                 using (marshaledCredential)
417                     return Marshal.PtrToStringUni(marshaledCredential.DangerousGetHandle());
418             }
419         }
420 
421         public static string UnmarshalCertificateCredential(string value)
422         {
423             NativeHelpers.CredMarshalType credType;
424             SafeCredentialBuffer pCredInfo;
425             if (!NativeMethods.CredUnmarshalCredentialW(value, out credType, out pCredInfo))
426                 throw new Win32Exception("CredUnmarshalCredentialW() failed");
427 
428             using (pCredInfo)
429             {
430                 if (credType != NativeHelpers.CredMarshalType.CertCredential)
431                     throw new InvalidOperationException(String.Format("Expected unmarshalled cred type of CertCredential, received {0}", credType));
432 
433                 byte[] structSizeBytes = new byte[sizeof(UInt32)];
434                 Marshal.Copy(pCredInfo.DangerousGetHandle(), structSizeBytes, 0, sizeof(UInt32));
435                 UInt32 structSize = BitConverter.ToUInt32(structSizeBytes, 0);
436 
437                 byte[] certInfoBytes = new byte[structSize];
438                 Marshal.Copy(pCredInfo.DangerousGetHandle(), certInfoBytes, 0, certInfoBytes.Length);
439 
440                 StringBuilder hex = new StringBuilder((certInfoBytes.Length - sizeof(UInt32)) * 2);
441                 for (int i = 4; i < certInfoBytes.Length; i++)
442                     hex.AppendFormat("{0:x2}", certInfoBytes[i]);
443 
444                 return hex.ToString().ToUpperInvariant();
445             }
446         }
447 
448         internal static void PtrToStructureArray<T>(T[] array, IntPtr ptr)
449         {
450             IntPtr ptrOffset = ptr;
451             for (int i = 0; i < array.Length; i++, ptrOffset = IntPtr.Add(ptrOffset, Marshal.SizeOf(typeof(T))))
452                 array[i] = (T)Marshal.PtrToStructure(ptrOffset, typeof(T));
453         }
454     }
455 }
456 '@
457 
458 $type = switch ($type) {
459     "domain_password" { [Ansible.CredentialManager.CredentialType]::DomainPassword }
460     "domain_certificate" { [Ansible.CredentialManager.CredentialType]::DomainCertificate }
461     "generic_password" { [Ansible.CredentialManager.CredentialType]::Generic }
462     "generic_certificate" { [Ansible.CredentialManager.CredentialType]::GenericCertificate }
463 }
464 
465 $credential = [Ansible.CredentialManager.Credential]::GetCredential($name, $type)
466 if ($null -ne $credential) {
467     $module.Result.exists = $true
468     $module.Result.alias = $credential.TargetAlias
469     $module.Result.attributes = [System.Collections.ArrayList]@()
470     $module.Result.comment = $credential.Comment
471     $module.Result.name = $credential.TargetName
472     $module.Result.persistence = $credential.Persist.ToString()
473     $module.Result.type = $credential.Type.ToString()
474     $module.Result.username = $credential.UserName
475 
476     if ($null -ne $credential.Secret) {
477         $module.Result.secret = [System.Convert]::ToBase64String($credential.Secret)
478     } else {
479         $module.Result.secret = $null
480     }
481 
482     foreach ($attribute in $credential.Attributes) {
483         $attribute_info = @{
484             name = $attribute.Keyword
485         }
486         if ($null -ne $attribute.Value) {
487             $attribute_info.data = [System.Convert]::ToBase64String($attribute.Value)
488         } else {
489             $attribute_info.data = $null
490         }
491         $module.Result.attributes.Add($attribute_info) > $null
492     }
493 } else {
494     $module.Result.exists = $false
495 }
496 
497 $module.ExitJson()
498 
499