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         alias = @{ type = "str" }
12         attributes = @{
13             type = "list"
14             elements = "dict"
15             options = @{
16                 name = @{ type = "str"; required = $true }
17                 data = @{ type = "str" }
18                 data_format = @{ type = "str"; default = "text"; choices = @("base64", "text") }
19             }
20         }
21         comment = @{ type = "str" }
22         name = @{ type = "str"; required = $true }
23         persistence = @{ type = "str"; default = "local"; choices = @("enterprise", "local") }
24         secret = @{ type = "str"; no_log = $true }
25         secret_format = @{ type = "str"; default = "text"; choices = @("base64", "text") }
26         state = @{ type = "str"; default = "present"; choices = @("absent", "present") }
27         type = @{
28             type = "str"
29             required = $true
30             choices = @("domain_password", "domain_certificate", "generic_password", "generic_certificate")
31         }
32         update_secret = @{ type = "str"; default = "always"; choices = @("always", "on_create") }
33         username = @{ type = "str" }
34     }
35     required_if = @(
36         ,@("state", "present", @("username"))
37     )
38     supports_check_mode = $true
39 }
40 
41 $module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
42 
43 $alias = $module.Params.alias
44 $attributes = $module.Params.attributes
45 $comment = $module.Params.comment
46 $name = $module.Params.name
47 $persistence = $module.Params.persistence
48 $secret = $module.Params.secret
49 $secret_format = $module.Params.secret_format
50 $state = $module.Params.state
51 $type = $module.Params.type
52 $update_secret = $module.Params.update_secret
53 $username = $module.Params.username
54 
55 $module.Diff.before = ""
56 $module.Diff.after = ""
57 
58 Add-CSharpType -AnsibleModule $module -References @'
59 using Microsoft.Win32.SafeHandles;
60 using System;
61 using System.Collections.Generic;
62 using System.Linq;
63 using System.Runtime.ConstrainedExecution;
64 using System.Runtime.InteropServices;
65 using System.Text;
66 
67 namespace Ansible.CredentialManager
68 {
69     internal class NativeHelpers
70     {
71         [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
72         public class CREDENTIAL
73         {
74             public CredentialFlags Flags;
75             public CredentialType Type;
76             [MarshalAs(UnmanagedType.LPWStr)] public string TargetName;
77             [MarshalAs(UnmanagedType.LPWStr)] public string Comment;
78             public FILETIME LastWritten;
79             public UInt32 CredentialBlobSize;
80             public IntPtr CredentialBlob;
81             public CredentialPersist Persist;
82             public UInt32 AttributeCount;
83             public IntPtr Attributes;
84             [MarshalAs(UnmanagedType.LPWStr)] public string TargetAlias;
85             [MarshalAs(UnmanagedType.LPWStr)] public string UserName;
86 
87             public static explicit operator Credential(CREDENTIAL v)
88             {
89                 byte[] secret = new byte[(int)v.CredentialBlobSize];
90                 if (v.CredentialBlob != IntPtr.Zero)
91                     Marshal.Copy(v.CredentialBlob, secret, 0, secret.Length);
92 
93                 List<CredentialAttribute> attributes = new List<CredentialAttribute>();
94                 if (v.AttributeCount > 0)
95                 {
96                     CREDENTIAL_ATTRIBUTE[] rawAttributes = new CREDENTIAL_ATTRIBUTE[v.AttributeCount];
97                     Credential.PtrToStructureArray(rawAttributes, v.Attributes);
98                     attributes = rawAttributes.Select(x => (CredentialAttribute)x).ToList();
99                 }
100 
101                 string userName = v.UserName;
102                 if (v.Type == CredentialType.DomainCertificate || v.Type == CredentialType.GenericCertificate)
103                     userName = Credential.UnmarshalCertificateCredential(userName);
104 
105                 return new Credential
106                 {
107                     Type = v.Type,
108                     TargetName = v.TargetName,
109                     Comment = v.Comment,
110                     LastWritten = (DateTimeOffset)v.LastWritten,
111                     Secret = secret,
112                     Persist = v.Persist,
113                     Attributes = attributes,
114                     TargetAlias = v.TargetAlias,
115                     UserName = userName,
116                     Loaded = true,
117                 };
118             }
119         }
120 
121         [StructLayout(LayoutKind.Sequential)]
122         public struct CREDENTIAL_ATTRIBUTE
123         {
124             [MarshalAs(UnmanagedType.LPWStr)] public string Keyword;
125             public UInt32 Flags;  // Set to 0 and is reserved
126             public UInt32 ValueSize;
127             public IntPtr Value;
128 
129             public static explicit operator CredentialAttribute(CREDENTIAL_ATTRIBUTE v)
130             {
131                 byte[] value = new byte[v.ValueSize];
132                 Marshal.Copy(v.Value, value, 0, (int)v.ValueSize);
133 
134                 return new CredentialAttribute
135                 {
136                     Keyword = v.Keyword,
137                     Flags = v.Flags,
138                     Value = value,
139                 };
140             }
141         }
142 
143         [StructLayout(LayoutKind.Sequential)]
144         public struct FILETIME
145         {
146             internal UInt32 dwLowDateTime;
147             internal UInt32 dwHighDateTime;
148 
149             public static implicit operator long(FILETIME v) { return ((long)v.dwHighDateTime << 32) + v.dwLowDateTime; }
150             public static explicit operator DateTimeOffset(FILETIME v) { return DateTimeOffset.FromFileTime(v); }
151             public static explicit operator FILETIME(DateTimeOffset v)
152             {
153                 return new FILETIME()
154                 {
155                     dwLowDateTime = (UInt32)v.ToFileTime(),
156                     dwHighDateTime = ((UInt32)v.ToFileTime() >> 32),
157                 };
158             }
159         }
160 
161         [Flags]
162         public enum CredentialCreateFlags : uint
163         {
164             PreserveCredentialBlob = 1,
165         }
166 
167         [Flags]
168         public enum CredentialFlags
169         {
170             None = 0,
171             PromptNow = 2,
172             UsernameTarget = 4,
173         }
174 
175         public enum CredMarshalType : uint
176         {
177             CertCredential = 1,
178             UsernameTargetCredential,
179             BinaryBlobCredential,
180             UsernameForPackedCredential,
181             BinaryBlobForSystem,
182         }
183     }
184 
185     internal class NativeMethods
186     {
187         [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
188         public static extern bool CredDeleteW(
189             [MarshalAs(UnmanagedType.LPWStr)] string TargetName,
190             CredentialType Type,
191             UInt32 Flags);
192 
193         [DllImport("advapi32.dll")]
194         public static extern void CredFree(
195             IntPtr Buffer);
196 
197         [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
198         public static extern bool CredMarshalCredentialW(
199             NativeHelpers.CredMarshalType CredType,
200             SafeMemoryBuffer Credential,
201             out SafeCredentialBuffer MarshaledCredential);
202 
203         [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
204         public static extern bool CredReadW(
205             [MarshalAs(UnmanagedType.LPWStr)] string TargetName,
206             CredentialType Type,
207             UInt32 Flags,
208             out SafeCredentialBuffer Credential);
209 
210         [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
211         public static extern bool CredUnmarshalCredentialW(
212             [MarshalAs(UnmanagedType.LPWStr)] string MarshaledCredential,
213             out NativeHelpers.CredMarshalType CredType,
214             out SafeCredentialBuffer Credential);
215 
216         [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
217         public static extern bool CredWriteW(
218             NativeHelpers.CREDENTIAL Credential,
219             NativeHelpers.CredentialCreateFlags Flags);
220     }
221 
222     internal class SafeCredentialBuffer : SafeHandleZeroOrMinusOneIsInvalid
223     {
224         public SafeCredentialBuffer() : base(true) { }
225 
226         [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
227         protected override bool ReleaseHandle()
228         {
229             NativeMethods.CredFree(handle);
230             return true;
231         }
232     }
233 
234     internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid
235     {
236         public SafeMemoryBuffer() : base(true) { }
237         public SafeMemoryBuffer(int cb) : base(true)
238         {
239             base.SetHandle(Marshal.AllocHGlobal(cb));
240         }
241         public SafeMemoryBuffer(IntPtr handle) : base(true)
242         {
243             base.SetHandle(handle);
244         }
245         [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
246         protected override bool ReleaseHandle()
247         {
248             Marshal.FreeHGlobal(handle);
249             return true;
250         }
251     }
252 
253     public class Win32Exception : System.ComponentModel.Win32Exception
254     {
255         private string _exception_msg;
256         public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
257         public Win32Exception(int errorCode, string message) : base(errorCode)
258         {
259             _exception_msg = String.Format("{0} - {1} (Win32 Error Code {2}: 0x{3})", message, base.Message, errorCode, errorCode.ToString("X8"));
260         }
261         public override string Message { get { return _exception_msg; } }
262         public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
263     }
264 
265     public enum CredentialPersist
266     {
267         Session = 1,
268         LocalMachine = 2,
269         Enterprise = 3,
270     }
271 
272     public enum CredentialType
273     {
274         Generic = 1,
275         DomainPassword = 2,
276         DomainCertificate = 3,
277         DomainVisiblePassword = 4,
278         GenericCertificate = 5,
279         DomainExtended = 6,
280         Maximum = 7,
281         MaximumEx = 1007,
282     }
283 
284     public class CredentialAttribute
285     {
286         public string Keyword;
287         public UInt32 Flags;
288         public byte[] Value;
289     }
290 
291     public class Credential
292     {
293         public CredentialType Type;
294         public string TargetName;
295         public string Comment;
296         public DateTimeOffset LastWritten;
297         public byte[] Secret;
298         public CredentialPersist Persist;
299         public List<CredentialAttribute> Attributes = new List<CredentialAttribute>();
300         public string TargetAlias;
301         public string UserName;
302 
303         // Used to track whether the credential has been loaded into the store or not
304         public bool Loaded { get; internal set; }
305 
306         public void Delete()
307         {
308             if (!Loaded)
309                 return;
310 
311             if (!NativeMethods.CredDeleteW(TargetName, Type, 0))
312                 throw new Win32Exception(String.Format("CredDeleteW({0}) failed", TargetName));
313             Loaded = false;
314         }
315 
316         public void Write(bool preserveExisting)
317         {
318             string userName = UserName;
319             // Convert the certificate thumbprint to the string expected
320             if (Type == CredentialType.DomainCertificate || Type == CredentialType.GenericCertificate)
321                 userName = Credential.MarshalCertificateCredential(userName);
322 
323             NativeHelpers.CREDENTIAL credential = new NativeHelpers.CREDENTIAL
324             {
325                 Flags = NativeHelpers.CredentialFlags.None,
326                 Type = Type,
327                 TargetName = TargetName,
328                 Comment = Comment,
329                 LastWritten = new NativeHelpers.FILETIME(),
330                 CredentialBlobSize = (UInt32)(Secret == null ? 0 : Secret.Length),
331                 CredentialBlob = IntPtr.Zero, // Must be allocated and freed outside of this to ensure no memory leaks
332                 Persist = Persist,
333                 AttributeCount = (UInt32)(Attributes.Count),
334                 Attributes = IntPtr.Zero, // Attributes must be allocated and freed outside of this to ensure no memory leaks
335                 TargetAlias = TargetAlias,
336                 UserName = userName,
337             };
338 
339             using (SafeMemoryBuffer credentialBlob = new SafeMemoryBuffer((int)credential.CredentialBlobSize))
340             {
341                 if (Secret != null)
342                     Marshal.Copy(Secret, 0, credentialBlob.DangerousGetHandle(), Secret.Length);
343                 credential.CredentialBlob = credentialBlob.DangerousGetHandle();
344 
345                 // Store the CREDENTIAL_ATTRIBUTE value in a safe memory buffer and make sure we dispose in all cases
346                 List<SafeMemoryBuffer> attributeBuffers = new List<SafeMemoryBuffer>();
347                 try
348                 {
349                     int attributeLength = Attributes.Sum(a => Marshal.SizeOf(typeof(NativeHelpers.CREDENTIAL_ATTRIBUTE)));
350                     byte[] attributeBytes = new byte[attributeLength];
351                     int offset = 0;
352                     foreach (CredentialAttribute attribute in Attributes)
353                     {
354                         SafeMemoryBuffer attributeBuffer = new SafeMemoryBuffer(attribute.Value.Length);
355                         attributeBuffers.Add(attributeBuffer);
356                         if (attribute.Value != null)
357                             Marshal.Copy(attribute.Value, 0, attributeBuffer.DangerousGetHandle(), attribute.Value.Length);
358 
359                         NativeHelpers.CREDENTIAL_ATTRIBUTE credentialAttribute = new NativeHelpers.CREDENTIAL_ATTRIBUTE
360                         {
361                             Keyword = attribute.Keyword,
362                             Flags = attribute.Flags,
363                             ValueSize = (UInt32)(attribute.Value == null ? 0 : attribute.Value.Length),
364                             Value = attributeBuffer.DangerousGetHandle(),
365                         };
366                         int attributeStructLength = Marshal.SizeOf(typeof(NativeHelpers.CREDENTIAL_ATTRIBUTE));
367 
368                         byte[] attrBytes = new byte[attributeStructLength];
369                         using (SafeMemoryBuffer tempBuffer = new SafeMemoryBuffer(attributeStructLength))
370                         {
371                             Marshal.StructureToPtr(credentialAttribute, tempBuffer.DangerousGetHandle(), false);
372                             Marshal.Copy(tempBuffer.DangerousGetHandle(), attrBytes, 0, attributeStructLength);
373                         }
374                         Buffer.BlockCopy(attrBytes, 0, attributeBytes, offset, attributeStructLength);
375                         offset += attributeStructLength;
376                     }
377 
378                     using (SafeMemoryBuffer attributes = new SafeMemoryBuffer(attributeBytes.Length))
379                     {
380                         if (attributeBytes.Length != 0)
381                         {
382                             Marshal.Copy(attributeBytes, 0, attributes.DangerousGetHandle(), attributeBytes.Length);
383                             credential.Attributes = attributes.DangerousGetHandle();
384                         }
385 
386                         NativeHelpers.CredentialCreateFlags createFlags = 0;
387                         if (preserveExisting)
388                             createFlags |= NativeHelpers.CredentialCreateFlags.PreserveCredentialBlob;
389 
390                         if (!NativeMethods.CredWriteW(credential, createFlags))
391                             throw new Win32Exception(String.Format("CredWriteW({0}) failed", TargetName));
392                     }
393                 }
394                 finally
395                 {
396                     foreach (SafeMemoryBuffer attributeBuffer in attributeBuffers)
397                         attributeBuffer.Dispose();
398                 }
399             }
400             Loaded = true;
401         }
402 
403         public static Credential GetCredential(string target, CredentialType type)
404         {
405             SafeCredentialBuffer buffer;
406             if (!NativeMethods.CredReadW(target, type, 0, out buffer))
407             {
408                 int lastErr = Marshal.GetLastWin32Error();
409 
410                 // Not running with Become so cannot manage the user's credentials
411                 if (lastErr == 0x00000520) // ERROR_NO_SUCH_LOGON_SESSION
412                     throw new InvalidOperationException("Failed to access the user's credential store, run the module with become");
413                 else if (lastErr == 0x00000490)  // ERROR_NOT_FOUND
414                     return null;
415                 throw new Win32Exception(lastErr, "CredEnumerateW() failed");
416             }
417 
418             using (buffer)
419             {
420                 NativeHelpers.CREDENTIAL credential = (NativeHelpers.CREDENTIAL)Marshal.PtrToStructure(
421                     buffer.DangerousGetHandle(), typeof(NativeHelpers.CREDENTIAL));
422                 return (Credential)credential;
423             }
424         }
425 
426         public static string MarshalCertificateCredential(string thumbprint)
427         {
428             // CredWriteW requires the UserName field to be the value of CredMarshalCredentialW() when writting a
429             // certificate auth. This converts the UserName property to the format required.
430 
431             // While CERT_CREDENTIAL_INFO is the correct structure, we manually marshal the data in order to
432             // support different cert hash lengths in the future.
433             // https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_cert_credential_info
434             int hexLength = thumbprint.Length;
435             byte[] credInfo = new byte[sizeof(UInt32) + (hexLength / 2)];
436 
437             // First field is cbSize which is a UInt32 value denoting the size of the total structure
438             Array.Copy(BitConverter.GetBytes((UInt32)credInfo.Length), credInfo, sizeof(UInt32));
439 
440             // Now copy the byte representation of the thumbprint to the rest of the struct bytes
441             for (int i = 0; i < hexLength; i += 2)
442                 credInfo[sizeof(UInt32) + (i / 2)] = Convert.ToByte(thumbprint.Substring(i, 2), 16);
443 
444             IntPtr pCredInfo = Marshal.AllocHGlobal(credInfo.Length);
445             Marshal.Copy(credInfo, 0, pCredInfo, credInfo.Length);
446             SafeMemoryBuffer pCredential = new SafeMemoryBuffer(pCredInfo);
447 
448             NativeHelpers.CredMarshalType marshalType = NativeHelpers.CredMarshalType.CertCredential;
449             using (pCredential)
450             {
451                 SafeCredentialBuffer marshaledCredential;
452                 if (!NativeMethods.CredMarshalCredentialW(marshalType, pCredential, out marshaledCredential))
453                     throw new Win32Exception("CredMarshalCredentialW() failed");
454                 using (marshaledCredential)
455                     return Marshal.PtrToStringUni(marshaledCredential.DangerousGetHandle());
456             }
457         }
458 
459         public static string UnmarshalCertificateCredential(string value)
460         {
461             NativeHelpers.CredMarshalType credType;
462             SafeCredentialBuffer pCredInfo;
463             if (!NativeMethods.CredUnmarshalCredentialW(value, out credType, out pCredInfo))
464                 throw new Win32Exception("CredUnmarshalCredentialW() failed");
465 
466             using (pCredInfo)
467             {
468                 if (credType != NativeHelpers.CredMarshalType.CertCredential)
469                     throw new InvalidOperationException(String.Format("Expected unmarshalled cred type of CertCredential, received {0}", credType));
470 
471                 byte[] structSizeBytes = new byte[sizeof(UInt32)];
472                 Marshal.Copy(pCredInfo.DangerousGetHandle(), structSizeBytes, 0, sizeof(UInt32));
473                 UInt32 structSize = BitConverter.ToUInt32(structSizeBytes, 0);
474 
475                 byte[] certInfoBytes = new byte[structSize];
476                 Marshal.Copy(pCredInfo.DangerousGetHandle(), certInfoBytes, 0, certInfoBytes.Length);
477 
478                 StringBuilder hex = new StringBuilder((certInfoBytes.Length - sizeof(UInt32)) * 2);
479                 for (int i = 4; i < certInfoBytes.Length; i++)
480                     hex.AppendFormat("{0:x2}", certInfoBytes[i]);
481 
482                 return hex.ToString().ToUpperInvariant();
483             }
484         }
485 
486         internal static void PtrToStructureArray<T>(T[] array, IntPtr ptr)
487         {
488             IntPtr ptrOffset = ptr;
489             for (int i = 0; i < array.Length; i++, ptrOffset = IntPtr.Add(ptrOffset, Marshal.SizeOf(typeof(T))))
490                 array[i] = (T)Marshal.PtrToStructure(ptrOffset, typeof(T));
491         }
492     }
493 }
494 '@
495 
ConvertTo-CredentialAttribute()496 Function ConvertTo-CredentialAttribute {
497     param($Attributes)
498 
499     $converted_attributes = [System.Collections.Generic.List`1[Ansible.CredentialManager.CredentialAttribute]]@()
500     foreach ($attribute in $Attributes) {
501         $new_attribute = New-Object -TypeName Ansible.CredentialManager.CredentialAttribute
502         $new_attribute.Keyword = $attribute.name
503 
504         if ($null -ne $attribute.data) {
505             if ($attribute.data_format -eq "base64") {
506                 $new_attribute.Value = [System.Convert]::FromBase64String($attribute.data)
507             } else {
508                 $new_attribute.Value = [System.Text.Encoding]::UTF8.GetBytes($attribute.data)
509             }
510         }
511         $converted_attributes.Add($new_attribute) > $null
512     }
513 
514     return ,$converted_attributes
515 }
516 
Get-DiffInfo()517 Function Get-DiffInfo {
518     param($AnsibleCredential)
519 
520     $diff = @{
521         alias = $AnsibleCredential.TargetAlias
522         attributes = [System.Collections.ArrayList]@()
523         comment = $AnsibleCredential.Comment
524         name = $AnsibleCredential.TargetName
525         persistence = $AnsibleCredential.Persist.ToString()
526         type = $AnsibleCredential.Type.ToString()
527         username = $AnsibleCredential.UserName
528     }
529 
530     foreach ($attribute in $AnsibleCredential.Attributes) {
531         $attribute_info = @{
532             name = $attribute.Keyword
533             data = $null
534         }
535         if ($null -ne $attribute.Value) {
536             $attribute_info.data = [System.Convert]::ToBase64String($attribute.Value)
537         }
538         $diff.attributes.Add($attribute_info) > $null
539     }
540 
541     return ,$diff
542 }
543 
544 # If the username is a certificate thumbprint, verify it's a valid cert in the CurrentUser/Personal store
545 if ($null -ne $username -and $type -in @("domain_certificate", "generic_certificate")) {
546     # Ensure the thumbprint is upper case with no spaces or hyphens
547     $username = $username.ToUpperInvariant().Replace(" ", "").Replace("-", "")
548 
549     $certificate = Get-Item -LiteralPath Cert:\CurrentUser\My\$username -ErrorAction SilentlyContinue
550     if ($null -eq $certificate) {
551         $module.FailJson("Failed to find certificate with the thumbprint $username in the CurrentUser\My store")
552     }
553 }
554 
555 # Convert the input secret to a byte array
556 if ($null -ne $secret) {
557     if ($secret_format -eq "base64") {
558         $secret = [System.Convert]::FromBase64String($secret)
559     } else {
560         $secret = [System.Text.Encoding]::Unicode.GetBytes($secret)
561     }
562 }
563 
564 $persistence = switch ($persistence) {
565     "local" { [Ansible.CredentialManager.CredentialPersist]::LocalMachine }
566     "enterprise" { [Ansible.CredentialManager.CredentialPersist]::Enterprise }
567 }
568 
569 $type = switch ($type) {
570     "domain_password" { [Ansible.CredentialManager.CredentialType]::DomainPassword }
571     "domain_certificate" { [Ansible.CredentialManager.CredentialType]::DomainCertificate }
572     "generic_password" { [Ansible.CredentialManager.CredentialType]::Generic }
573     "generic_certificate" { [Ansible.CredentialManager.CredentialType]::GenericCertificate }
574 }
575 
576 $existing_credential = [Ansible.CredentialManager.Credential]::GetCredential($name, $type)
577 if ($null -ne $existing_credential) {
578     $module.Diff.before = Get-DiffInfo -AnsibleCredential $existing_credential
579 }
580 
581 if ($state -eq "absent") {
582     if ($null -ne $existing_credential) {
583         if (-not $module.CheckMode) {
584             $existing_credential.Delete()
585         }
586         $module.Result.changed = $true
587     }
588 } else {
589     if ($null -eq $existing_credential) {
590         $new_credential = New-Object -TypeName Ansible.CredentialManager.Credential
591         $new_credential.Type = $type
592         $new_credential.TargetName = $name
593         $new_credential.Comment = if ($comment) { $comment } else { [NullString]::Value }
594         $new_credential.Secret = $secret
595         $new_credential.Persist = $persistence
596         $new_credential.TargetAlias = if ($alias) { $alias } else { [NullString]::Value }
597         $new_credential.UserName = $username
598 
599         if ($null -ne $attributes) {
600             $new_credential.Attributes = ConvertTo-CredentialAttribute -Attributes $attributes
601         }
602 
603         if (-not $module.CheckMode) {
604             $new_credential.Write($false)
605         }
606         $module.Result.changed = $true
607     } else {
608         $changed = $false
609         $preserve_blob = $false
610 
611         # make sure we do case comparison for the comment
612         if ($existing_credential.Comment -cne $comment) {
613             $existing_credential.Comment = $comment
614             $changed = $true
615         }
616 
617         if ($existing_credential.Persist -ne $persistence) {
618             $existing_credential.Persist = $persistence
619             $changed = $true
620         }
621 
622         if ($existing_credential.TargetAlias -ne $alias) {
623             $existing_credential.TargetAlias = $alias
624             $changed = $true
625         }
626 
627         if ($existing_credential.UserName -ne $username) {
628             $existing_credential.UserName = $username
629             $changed = $true
630         }
631 
632         if ($null -ne $attributes) {
633             $attribute_changed = $false
634 
635             $new_attributes = ConvertTo-CredentialAttribute -Attributes $attributes
636             if ($new_attributes.Count -ne $existing_credential.Attributes.Count) {
637                 $attribute_changed = $true
638             } else {
639                 for ($i = 0; $i -lt $new_attributes.Count; $i++) {
640                     $new_keyword = $new_attributes[$i].Keyword
641                     $new_value = $new_attributes[$i].Value
642                     if ($null -eq $new_value) {
643                         $new_value = ""
644                     } else {
645                         $new_value = [System.Convert]::ToBase64String($new_value)
646                     }
647 
648                     $existing_keyword = $existing_credential.Attributes[$i].Keyword
649                     $existing_value = $existing_credential.Attributes[$i].Value
650                     if ($null -eq $existing_value) {
651                         $existing_value = ""
652                     } else {
653                         $existing_value = [System.Convert]::ToBase64String($existing_value)
654                     }
655 
656                     if (($new_keyword -cne $existing_keyword) -or ($new_value -ne $existing_value)) {
657                         $attribute_changed = $true
658                         break
659                     }
660                 }
661             }
662 
663             if ($attribute_changed) {
664                 $existing_credential.Attributes = $new_attributes
665                 $changed = $true
666             }
667         }
668 
669         if ($null -eq $secret) {
670             # If we haven't explicitly set a secret, tell Windows to preserve the existing blob
671             $preserve_blob = $true
672             $existing_credential.Secret = $null
673         } elseif ($update_secret -eq "always") {
674             # We should only set the password if we can't read the existing one or it doesn't match our secret
675             if ($existing_credential.Secret.Length -eq 0) {
676                 # We cannot read the secret so don't know if its the configured secret
677                 $existing_credential.Secret = $secret
678                 $changed = $true
679             } else {
680                 # We can read the secret so compare with our input
681                 $input_secret_b64 = [System.Convert]::ToBase64String($secret)
682                 $actual_secret_b64 = [System.Convert]::ToBase64String($existing_credential.Secret)
683                 if ($input_secret_b64 -ne $actual_secret_b64) {
684                     $existing_credential.Secret = $secret
685                     $changed = $true
686                 }
687             }
688         }
689 
690         if ($changed -and -not $module.CheckMode) {
691             $existing_credential.Write($preserve_blob)
692         }
693         $module.Result.changed = $changed
694     }
695 
696     if ($module.CheckMode) {
697         # We cannot reliably get the credential in check mode, set it based on the input
698         $module.Diff.after = @{
699             alias = $alias
700             attributes = $attributes
701             comment = $comment
702             name = $name
703             persistence = $persistence.ToString()
704             type = $type.ToString()
705             username = $username
706         }
707     } else {
708         # Get a new copy of the credential and use that to set the after diff
709         $new_credential = [Ansible.CredentialManager.Credential]::GetCredential($name, $type)
710         $module.Diff.after = Get-DiffInfo -AnsibleCredential $new_credential
711     }
712 }
713 
714 $module.ExitJson()
715