├── TestCertificateFile.cer
├── TestCertificateFile.pfx
├── Security.Cryptography.dll
├── TestCertificateFile2.pfx
├── TestCertificateFile3.pfx
├── ProtectedData.psm1
├── V1.0.ProtectedWithTestCertificateFile.pfx.xml
├── HMAC.ps1
├── ProtectedData.psd1
├── PinnedArray.ps1
├── README.md
├── en-US
└── about_ProtectedData.help.txt
├── TestUtils.ps1
├── LICENSE
├── ProtectedData.Tests.ps1
└── Commands.ps1
/TestCertificateFile.cer:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dlwyatt/ProtectedData/HEAD/TestCertificateFile.cer
--------------------------------------------------------------------------------
/TestCertificateFile.pfx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dlwyatt/ProtectedData/HEAD/TestCertificateFile.pfx
--------------------------------------------------------------------------------
/Security.Cryptography.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dlwyatt/ProtectedData/HEAD/Security.Cryptography.dll
--------------------------------------------------------------------------------
/TestCertificateFile2.pfx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dlwyatt/ProtectedData/HEAD/TestCertificateFile2.pfx
--------------------------------------------------------------------------------
/TestCertificateFile3.pfx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dlwyatt/ProtectedData/HEAD/TestCertificateFile3.pfx
--------------------------------------------------------------------------------
/ProtectedData.psm1:
--------------------------------------------------------------------------------
1 | $path = Split-Path $MyInvocation.MyCommand.Path
2 |
3 | Add-Type -Path $path\Security.Cryptography.dll -ErrorAction Stop
4 |
5 | . $path\PinnedArray.ps1
6 | . $path\HMAC.ps1
7 | . $path\Commands.ps1
8 |
--------------------------------------------------------------------------------
/V1.0.ProtectedWithTestCertificateFile.pfx.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | System.Management.Automation.PSCustomObject
5 | System.Object
6 |
7 |
8 |
9 |
10 | System.Object[]
11 | System.Array
12 | System.Object
13 |
14 |
15 |
16 |
17 |
18 | BC7BC5DD2AA4F757FC0FB2944082F765FFFCA88C
19 | G072lXT2KSybTvBYyCxJdDw35sCk4WJD3/t9gTWEEZvqs+40HNbmywlKlSS8bmC8avKtaXSkI+n1/Y+Om0vLK7K/ywz5apzXE1Y9VaTNgLHLepsr7N38vXg5eD055ZEily4fIoZTKtfAlXTR3KoALrcTdMTWh55dKL3/zpDQGll9bJB+0BukTY0gaCYWPLsCQlw6iTrkvE17ISh+40lPDWSgdLLSRVIIP9jNqn13uj4fsTp1Ui+ZL1lq2w7SqS6GiqrqSISSGaZ0r/LA7y+uT76+3zcCMRKT3tjia3tYMnsA3zKZLJZ4s4RfcSuL1ox1LjGoWgLHqo5Mw+QbbnEM5A==
20 | zhrR/YJBub+B1nNcoTTZD/fOBXEP41//ASiAjr4/YVmDrbxvyJzfdC6lN+4Xzhcu1xmle4AbIh96wYqI3Yk0Y4JW7Si+EJnvV4Rp8CHU7LyV1zc8z1tecfNydqG6lHkMQcYKbZi2JMle+H8xQuwEizyLGMni/4tU/aJpCV+N3x+6uKGdH49c+JpPAVucKbJX49xCjoAmPT81TQMmpj4nUPUASN6qG3jMnfmMGtWJa9Kd2AdM9zOFUfhF2lktKIQK5fqJxZROTfLINiBlRPcwfbXhI49M3PLYiDueaLYWHJ0dxDArocwb7r747T1jt+pwlK+/ejXrq7zPNiUs0UwCDA==
21 |
22 |
23 |
24 |
25 | 01ely5CfqOFUiKxphIrdvQ==
26 | System.String
27 |
28 |
29 |
--------------------------------------------------------------------------------
/HMAC.ps1:
--------------------------------------------------------------------------------
1 | Add-Type -ReferencedAssemblies System.Core -WarningAction SilentlyContinue -TypeDefinition @'
2 | namespace PowerShellUtils
3 | {
4 | using System;
5 | using System.Reflection;
6 | using System.Security.Cryptography;
7 |
8 | public class FipsHmacSha256 : HMAC
9 | {
10 | // Class exists to guarantee FIPS compliant SHA-256 HMAC, which isn't
11 | // the case in the built-in HMACSHA256 class in older version of the
12 | // .NET Framework and PowerShell.
13 |
14 | private static RNGCryptoServiceProvider rng;
15 |
16 | private static RNGCryptoServiceProvider Rng
17 | {
18 | get
19 | {
20 | if (rng == null)
21 | {
22 | rng = new RNGCryptoServiceProvider();
23 | }
24 |
25 | return rng;
26 | }
27 | }
28 |
29 | private static byte[] GetRandomBytes(int keyLength)
30 | {
31 | byte[] array = new byte[keyLength];
32 | Rng.GetBytes(array);
33 | return array;
34 | }
35 |
36 | public FipsHmacSha256() : this(GetRandomBytes(64)) { }
37 |
38 | public FipsHmacSha256(byte[] key)
39 | {
40 | BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic;
41 |
42 | typeof(HMAC).GetField("m_hashName", flags).SetValue(this, "SHA256");
43 | typeof(HMAC).GetField("m_hash1", flags).SetValue(this, new SHA256CryptoServiceProvider());
44 | typeof(HMAC).GetField("m_hash2", flags).SetValue(this, new SHA256CryptoServiceProvider());
45 |
46 | HashSizeValue = 256;
47 | Key = key;
48 | }
49 | }
50 | }
51 | '@
52 |
--------------------------------------------------------------------------------
/ProtectedData.psd1:
--------------------------------------------------------------------------------
1 | #
2 | # Module manifest for module 'ProtectedData'
3 | #
4 | # Generated by: Dave Wyatt
5 | #
6 | # Generated on: 5/26/2014
7 | #
8 |
9 | @{
10 | ModuleToProcess = 'ProtectedData.psm1'
11 | ModuleVersion = '4.1.3'
12 | GUID = 'fc6a2f6a-563d-422a-85b5-9638e45a370e'
13 | Author = 'Dave Wyatt'
14 | CompanyName = 'Home'
15 | Copyright = 'Copyright 2014 Dave Wyatt'
16 | Description = 'Encrypt and share secret data between different users and computers.'
17 | PowerShellVersion = '2.0'
18 | DotNetFrameworkVersion = '3.5'
19 | FunctionsToExport = 'Protect-Data', 'Unprotect-Data', 'Get-ProtectedDataSupportedTypes',
20 | 'Add-ProtectedDataCredential', 'Remove-ProtectedDataCredential',
21 | 'Get-KeyEncryptionCertificate', 'Add-ProtectedDataHmac'
22 |
23 | PrivateData = @{
24 | PSData = @{
25 | # The primary categorization of this module (from the TechNet Gallery tech tree).
26 | Category = 'Scripting Techniques'
27 |
28 | # Keyword tags to help users find this module via navigations and search.
29 | Tags = @('powershell','encryption')
30 |
31 | # The web address of this module's project or support homepage.
32 | ProjectUri = 'https://github.com/dlwyatt/ProtectedData'
33 |
34 | # The web address of this module's license. Points to a page that's embeddable and linkable.
35 | LicenseUri = 'https://www.apache.org/licenses/LICENSE-2.0.html'
36 |
37 | # Indicates this is a pre-release/testing version of the module.
38 | IsPrerelease = 'False'
39 |
40 | ReleaseNotes = 'Updated parameter names to be compatible with latest PowerShell 5.0 Preview.'
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/PinnedArray.ps1:
--------------------------------------------------------------------------------
1 | Add-Type -TypeDefinition @'
2 | namespace PowerShellUtils
3 | {
4 | using System;
5 | using System.Runtime.InteropServices;
6 |
7 | public sealed class PinnedArray : IDisposable
8 | {
9 | private readonly T[] array;
10 | private readonly GCHandle gcHandle;
11 |
12 | private bool isDisposed = false;
13 |
14 | public static implicit operator T[](PinnedArray pinnedArray)
15 | {
16 | return pinnedArray.Array;
17 | }
18 |
19 | public T this[int key]
20 | {
21 | get
22 | {
23 | if (isDisposed) { throw new ObjectDisposedException("PinnedArray"); }
24 | return array[key];
25 | }
26 |
27 | set
28 | {
29 | if (isDisposed) { throw new ObjectDisposedException("PinnedArray"); }
30 | array[key] = value;
31 | }
32 | }
33 |
34 | public T[] Array
35 | {
36 | get
37 | {
38 | if (isDisposed) { throw new ObjectDisposedException("PinnedArray"); }
39 | return array;
40 | }
41 | }
42 |
43 | public int Length
44 | {
45 | get
46 | {
47 | if (isDisposed) { throw new ObjectDisposedException("PinnedArray"); }
48 | return array.Length;
49 | }
50 | }
51 |
52 | public int Count
53 | {
54 | get { return Length; }
55 | }
56 |
57 | public PinnedArray(uint count)
58 | {
59 | array = new T[count];
60 | gcHandle = GCHandle.Alloc(Array, GCHandleType.Pinned);
61 | }
62 |
63 | public PinnedArray(T[] array)
64 | {
65 | if (array == null) { throw new ArgumentNullException("array"); }
66 |
67 | this.array = array;
68 | gcHandle = GCHandle.Alloc(this.array, GCHandleType.Pinned);
69 | }
70 |
71 | ~PinnedArray()
72 | {
73 | Dispose();
74 | }
75 |
76 | public void Dispose()
77 | {
78 | if (isDisposed) { return; }
79 |
80 | if (array != null) { System.Array.Clear(array, 0, array.Length); }
81 | if (gcHandle != null) { gcHandle.Free(); }
82 |
83 | isDisposed = true;
84 | }
85 | }
86 | }
87 | '@
88 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | __Build Status:__ [/statusIcon)](https://build.powershell.org/project.html?projectId=ProtectedData&tab=projectOverview&guest=1)
2 |
3 | ProtectedData
4 | =============
5 |
6 | PowerShell Module for securely encrypting and sharing secret data.
7 |
8 | Passwords, other encryption keys, your secret family recipe for baked beans, whatever! If you don't want to store something in the clear, and need to be able to decrypt the data as more than one user (or on more than one computer), this module can help.
9 |
10 | Special thanks to Vadims Podāns ([PowerShell Crypto Guy](http://en-us.sysadmins.lv/default.aspx)), whose feedback, ideas and code contributed greatly to the features that have been added to this module since its v1.0 release - in particular, support for CNG certificates and keys.
11 |
12 | ## Isn't this just like Protect-CmsMessage and Unprotect-CmsMessage?
13 |
14 | Very similar, yes! I was writing this module pretty much at the same time that the PowerShell team was working on the v5 previews that first gave us the CmsMessage cmdlets. The timing was unfortunate; had I known what the PS team was working on, I'd have simply backported their commands to work on older versions of PowerShell.
15 |
16 | Here are the basic pros and cons comparing the built-in CmsMessage commands and the ProtectedData modle:
17 |
18 | - The CmsMessage commands produce standards-based output, which can enable some cross-platform interaction with your PowerShell scripts. The ProtectedData module, on the other hand, produces PowerShell objects that are intended to be decrypted by the same PS module.
19 | - The CmsMessage commands are only available in PowerShell v4 (with latest patches) and v5. ProtectedData is compatible down to PowerShell v2.
20 | - The CmsMessage commands are really only useful for encrypting strings of text. If you pass them a complex object, what gets encrypted is the string that results from piping that object to `Out-String`. Most of the time you can get around this by running your object through something like ConvertTo-Json first, but SecureString and PSCredential objects are a bit more of a pain (as you must decrypt the SecureString to plain text before passing it on to Protect-CmsMessage for encryption.) ProtectedData, on the other hand, supports strings, SecureStrings, PSCredentials, and byte arrays without any additional effort from the caller.
21 | - ProtectedData supports CNG (Crypto Next Generation) certificate and keys, and Elliptic Curve encryption. As of this writing, the CmsMessage commands do not support these types of certificates / keys, mainly because the underlying .NET framework still doesn't have built-in support for them. However, I believe that CNG support is coming to the .NET framework as of v4.6, and the CmsMessage commands may simply "inherit" CNG support from .NET at some point in the future.
22 |
23 | I gave a presentation at the PowerShell Summit which includes demonstrations of both of these modules, with comparisons of functionality. It's available on YouTube at https://www.youtube.com/watch?v=Ta2hQHVKauo
24 |
--------------------------------------------------------------------------------
/en-US/about_ProtectedData.help.txt:
--------------------------------------------------------------------------------
1 | TOPIC
2 | about_ProtectedData
3 |
4 | SHORT DESCRIPTION
5 | Provides background information about the ProtectedData module.
6 |
7 | About ProtectedData
8 | When you need to store secret data, such as a set of credentials, in a
9 | PowerShell script, you would typically use the Export-Clixml or
10 | ConvertFrom-SecureString cmdlets to accomplish this. These commands
11 | leverage the Windows Data Protection API (DPAPI) to perform the encryption.
12 |
13 | The DPAPI is extremely convenient, but it has a limitation: the data can
14 | only be decrypted by the user who originally encrypted it, and in many cases,
15 | this decryption can only happen on the same computer where the encryption
16 | took place (unless you have Credential Roaming or Roaming Profiles enabled
17 | in an Active Directory environment.)
18 |
19 | The ProtectedData module exists to overcome this limitation, while still
20 | allowing the convenience of not having to worry about managing or protecting
21 | encryption keys. It does this, primarily, by leveraging digital certificates.
22 |
23 | Note: The latest versions of PowerShell have new cmdlets called Protect-CmsMessage
24 | and Unprotect-CmsMessage which accomplish a very similar task. The ProtectedData
25 | module is compatible all the way back to PowerShell 2.0, though, and has some
26 | features that the CmsMessage cmdlets do not.
27 |
28 | How It Works
29 | When you send a piece of data to the Protect-Data command, it is encrypted
30 | using a randomly-generated AES key and initialization vector (IV). Copies
31 | of this key and IV are then encrypted using either the public keys of RSA
32 | or ECDH certificates, or using a password-derived AES key (more on that later.)
33 |
34 | The resulting object can be persisted to disk with Export-Clixml, and can
35 | later be read back in with Import-Clixml and then passed to the
36 | Unprotect-Data command. When you call Unprotect-Data, you must pass in either
37 | one of the passwords that was to protect the data, or the thumbprint of one
38 | of the certificates that was used to protect the data. If you use a certificate
39 | when calling Unprotect-Data, you must have the certificate's private key.
40 |
41 | Regarding Password-derived Keys
42 | This module's intended use is to leverage certificate-based encryption wherever
43 | possible. This is what provides security, without the need for the user to worry
44 | about key protection or key management; the operating system takes care of this for
45 | you when you install a certificate (with or without its private key.)
46 |
47 | The various -Password parameters to the ProtectedData module's commands are
48 | intended as a backup mechanism. If you are unable to decrypt the data with
49 | a certificate for some reason, you'd be able to enter the correct password
50 | to retrieve it or to add a new certificate-encrypted copy of the keys.
51 |
52 | If you do use the Password functionality of the module, you're encouraged to
53 | always enter these passwords interactively. If you try to persist the passwords
54 | in some way, you're back to the original problem: you can either use DPAPI
55 | and accept its limitations, or you have to manage and protect encryption keys
56 | yourself.
57 |
58 | All passwords are passed to the ProtectedData commands in the form of
59 | SecureString objects.
60 |
61 | Supported Data Types
62 | All data must be serialized to a byte array before it can be encrypted. The
63 | ProtectedData module supports automatic serialization / deserialization of
64 | PSCredential, SecureString, and String objects. If you want to encrypt another
65 | data type instead, you're responsible for converting it to a byte array yourself
66 | first, and passing the resulting byte array to Protect-Data's -InputObject
67 | parameter.
68 |
69 | The ProtectedData object which is returned from the Protect-Data command includes
70 | a Type property. When you pass the object to Unprotect-Data, it uses this information
71 | to build and return an object of the original type for you (PSCredential, SecureString,
72 | String or Byte[] .)
73 |
74 | Regarding In-Memory Security
75 | The commands in the ProtectedData module make an effort to minimize the amount
76 | of time that any sensitive, unencrypted data is left in memory as well, but
77 | this is a tricky topic in a .NET application. The Garbage Collector can
78 | sometimes create copies of unencrypted byte arrays before the module has had
79 | a chance to pin them. This in-memory security is provided on a "best effort"
80 | basis.
81 |
82 | Certificate requirements
83 | The RSA certificates used with this module must allow Key Encipherment in their
84 | Key Usage extension. ECDH certificate must allow the Key Agreement Key Usage
85 | extension.
86 |
87 | You can verify which of your certificates are usable for both encryption and
88 | decryption ahead of time by running the following command:
89 |
90 | Get-KeyEncryptionCertificate -RequirePrivateKey
91 |
92 | (With this set of parameters, the command searches the entire Cert: drive, including
93 | both CurrentUser and LocalMachine stores.)
94 |
95 | SEE ALSO
96 | Protect-Data
97 | Unprotect-Data
98 | Add-ProtectedDataCredential
99 | Remove-ProtectedDataCredential
100 | Get-ProtectedDataSupportedTypes
101 | Get-KeyEncryptionCertificate
102 |
--------------------------------------------------------------------------------
/TestUtils.ps1:
--------------------------------------------------------------------------------
1 | function Get-PlainTextFromSecureString
2 | {
3 | [CmdletBinding()]
4 | param (
5 | [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
6 | [System.Security.SecureString]
7 | $SecureString
8 | )
9 |
10 | process
11 | {
12 | $ptr = $null
13 |
14 | try
15 | {
16 | $ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToGlobalAllocUnicode($SecureString)
17 | [System.Runtime.InteropServices.Marshal]::PtrToStringUni($ptr)
18 | }
19 | finally
20 | {
21 | if ($null -ne $ptr) { [System.Runtime.InteropServices.Marshal]::ZeroFreeGlobalAllocUnicode($ptr) }
22 | }
23 | }
24 | }
25 |
26 | function New-TestCertificate
27 | {
28 | [CmdletBinding()]
29 | param (
30 | [Parameter(Mandatory = $true)]
31 | [string]
32 | $Subject,
33 |
34 | [Nullable[DateTime]]
35 | $NotBefore,
36 |
37 | [Nullable[DateTime]]
38 | $NotAfter,
39 |
40 | [ValidateSet('Rsa', 'RsaCng', 'Ecdh_P256', 'Ecdh_P384', 'Ecdh_P521')]
41 | [string]
42 | $CertificateType = 'Rsa',
43 |
44 | [switch]
45 | $NoKeyUsageExtension
46 | )
47 |
48 | if ($null -ne $NotBefore -and $null -ne $NotAfter -and $NotBefore -ge $NotAfter)
49 | {
50 | throw 'NotAfter date/time must take place after NotBefore'
51 | }
52 |
53 | $notBeforeString = $notAfterString = ''
54 |
55 | if ($null -ne $NotBefore)
56 | {
57 | $notBeforeString = "NotBefore = ""$($NotBefore.ToString('G'))"""
58 | }
59 |
60 | if ($null -ne $NotAfter)
61 | {
62 | $notAfterString = "NotAfter = ""$($NotAfter.ToString('G'))"""
63 | }
64 |
65 | $requestfile = [System.IO.Path]::GetTempFileName()
66 | $certFile = [System.IO.Path]::GetTempFileName()
67 |
68 | switch ($CertificateType)
69 | {
70 | 'Rsa'
71 | {
72 | $providerName = 'Microsoft RSA SChannel Cryptographic Provider'
73 | $keyLength = 2048
74 | $keyAlgorithm = ''
75 | $keySpec = 'KeySpec = AT_KEYEXCHANGE'
76 | $keyUsage = 'CERT_KEY_ENCIPHERMENT_KEY_USAGE'
77 | $providerType = 12
78 |
79 | break
80 | }
81 |
82 | 'RsaCng'
83 | {
84 | $providerName = 'Microsoft Software Key Storage Provider'
85 | $keyLength = 2048
86 | $keyAlgorithm = ''
87 | $keySpec = 'KeySpec = AT_KEYEXCHANGE'
88 | $keyUsage = 'CERT_KEY_ENCIPHERMENT_KEY_USAGE'
89 | $providerType = 0
90 |
91 | break
92 | }
93 |
94 | 'Ecdh_P256'
95 | {
96 | $providerName = 'Microsoft Software Key Storage Provider'
97 | $keyLength = 256
98 | $keyAlgorithm = 'KeyAlgorithm = ECDH_P256'
99 | $keySpec = ''
100 | $keyUsage = 'CERT_KEY_AGREEMENT_KEY_USAGE'
101 | $providerType = 0
102 |
103 | break
104 | }
105 |
106 | 'Ecdh_P384'
107 | {
108 | $providerName = 'Microsoft Software Key Storage Provider'
109 | $keyLength = 384
110 | $keyAlgorithm = 'KeyAlgorithm = ECDH_P384'
111 | $keySpec = ''
112 | $keyUsage = 'CERT_KEY_AGREEMENT_KEY_USAGE'
113 | $providerType = 0
114 |
115 | break
116 | }
117 |
118 | 'Ecdh_P521'
119 | {
120 | $providerName = 'Microsoft Software Key Storage Provider'
121 | $keyLength = 521
122 | $keyAlgorithm = 'KeyAlgorithm = ECDH_P521'
123 | $keySpec = ''
124 | $keyUsage = 'CERT_KEY_AGREEMENT_KEY_USAGE'
125 | $providerType = 0
126 |
127 | break
128 | }
129 | }
130 |
131 | Set-Content -Path $requestfile -Encoding Ascii -Value @"
132 | [Version]
133 | Signature="`$Windows NT`$"
134 |
135 | [NewRequest]
136 | Subject = "$Subject"
137 | KeyLength = $keyLength
138 | Exportable = TRUE
139 | FriendlyName = "ProtectedData"
140 | ProviderName = "$providerName"
141 | ProviderType = $providerType
142 | RequestType = Cert
143 | Silent = True
144 | SuppressDefaults = True
145 | $keySpec
146 | $keyAlgorithm
147 | $(
148 | if (-not $NoKeyUsageExtension)
149 | {
150 | "KeyUsage = $keyUsage"
151 | }
152 | )
153 | $notBeforeString
154 | $notAfterString
155 |
156 | [EnhancedKeyUsageExtension]
157 | OID = 1.3.6.1.4.1.311.80.1
158 | "@
159 |
160 | try
161 | {
162 | $oldCerts = @(
163 | Get-ChildItem Cert:\CurrentUser\My |
164 | Where-Object { $_.Subject -eq $Subject } |
165 | Select-Object -ExpandProperty Thumbprint
166 | )
167 |
168 | $result = certreq.exe -new -f -q $requestfile $certFile
169 |
170 | if ($LASTEXITCODE -ne 0)
171 | {
172 | throw $result
173 | }
174 |
175 | $newCert = Get-ChildItem Cert:\CurrentUser\My -Exclude $oldCerts |
176 | Where-Object { $_.Subject -eq $Subject } |
177 | Select-Object -ExpandProperty Thumbprint
178 |
179 | return $newCert
180 | }
181 | finally
182 | {
183 | Remove-Item -Path $requestfile -Force -ErrorAction SilentlyContinue
184 | Remove-Item -Path $certFile -Force -ErrorAction SilentlyContinue
185 |
186 | if (Test-Path Cert:\CurrentUser\CA\$newCert)
187 | {
188 | try
189 | {
190 | $store = Get-Item Cert:\CurrentUser\CA
191 | $store.Open('ReadWrite')
192 |
193 | $cert = Get-Item Cert:\CurrentUser\CA\$newCert
194 |
195 | $store.Remove($cert)
196 |
197 | $store.Close()
198 | }
199 | catch { }
200 | }
201 | }
202 | }
203 |
204 | function Remove-TestCertificate
205 | {
206 | $path = 'Cert:\CurrentUser\My'
207 |
208 | $oldCerts = @(
209 | Get-ChildItem $path |
210 | Where-Object { $_.Subject -eq $testCertificateSubject }
211 | )
212 |
213 | if ($oldCerts.Count -gt 0)
214 | {
215 | $store = Get-Item $path
216 | $store.Open('ReadWrite')
217 |
218 | foreach ($oldCert in $oldCerts)
219 | {
220 | $store.Remove($oldCert)
221 | }
222 |
223 | $store.Close()
224 | }
225 | }
226 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/ProtectedData.Tests.ps1:
--------------------------------------------------------------------------------
1 | Import-Module Pester -ErrorAction Stop
2 |
3 | Set-StrictMode -Version Latest
4 |
5 | $scriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
6 | $moduleManifest = Join-Path -Path $scriptRoot -ChildPath ProtectedData.psd1
7 |
8 | . $scriptRoot\TestUtils.ps1
9 |
10 | # Neat wildcard trick for removing a module if it's loaded, without producing errors if it's not.
11 | Remove-Module [P]rotectedData
12 | Import-Module $moduleManifest -Force -ErrorAction Stop
13 |
14 | $stringToEncrypt = 'This is my string.'
15 | $secureStringToEncrypt = $stringToEncrypt | ConvertTo-SecureString -AsPlainText -Force
16 |
17 | $userName = 'UserName'
18 | $credentialToEncrypt = New-Object System.Management.Automation.PSCredential($userName, $secureStringToEncrypt)
19 |
20 | $byteArrayToEncrypt = [byte[]](1..10)
21 |
22 | $passwordForEncryption = 'p@ssw0rd' | ConvertTo-SecureString -AsPlainText -Force
23 | $wrongPassword = 'wr0ngp@ssw0rd' | ConvertTo-SecureString -AsPlainText -Force
24 |
25 | $testCertificateSubject = 'CN=ProtectedData Test Certificate, OU=Unit Tests, O=ProtectedData, L=Somewhere, S=Ontario, C=CA'
26 |
27 | Describe 'Password-based encryption and decryption' {
28 | $iterationCount = 20
29 |
30 | Context 'General Usage' {
31 | $blankSecureString = New-Object System.Security.SecureString
32 | $blankSecureString.MakeReadOnly()
33 |
34 | $secondPassword = 'Some other password' | ConvertTo-SecureString -AsPlainText -Force
35 |
36 | It 'Produces an error if a blank password is used' {
37 | { $null = Protect-Data -InputObject $stringToEncrypt -Password $blankSecureString -ErrorAction Stop -PasswordIterationCount $iterationCount } | Should Throw
38 | }
39 |
40 | It 'Does not produce an error when a non-blank password is used' {
41 | { $null = Protect-Data -InputObject $stringToEncrypt -Password $passwordForEncryption -ErrorAction Stop -PasswordIterationCount $iterationCount } | Should Not Throw
42 | }
43 |
44 | $protected = Protect-Data -InputObject $stringToEncrypt -Password $passwordForEncryption, $secondPassword -PasswordIterationCount $iterationCount
45 |
46 | It 'Produces an error if a decryption attempt with the wrong password is made.' {
47 | { $null = Unprotect-Data -InputObject $protected -Password $wrongPassword -ErrorAction Stop } | Should Throw
48 | }
49 |
50 | It 'Allows any of the passwords to be used when decrypting. (First password test)' {
51 | { $null = Unprotect-Data -InputObject $protected -Password $passwordForEncryption -ErrorAction Stop } | Should Not Throw
52 | }
53 |
54 | It 'Allows any of the passwords to be used when decrypting. (Second password test)' {
55 | { $null = Unprotect-Data -InputObject $protected -Password $secondPassword -ErrorAction Stop } | Should Not Throw
56 | }
57 |
58 | It 'Adds a new password to an existing object' {
59 | { Add-ProtectedDataCredential -InputObject $protected -Password $passwordForEncryption -NewPassword $wrongPassword -ErrorAction Stop -PasswordIterationCount $iterationCount } |
60 | Should Not Throw
61 | }
62 |
63 | It 'Uses the proper iteration count when Add-ProtectedDataCredential was called' {
64 | $wrong = $protected.KeyData | Where-Object { $null -ne $_.PSObject.Properties['IterationCount'] -and $_.IterationCount -ne $iterationCount }
65 | $wrong | Should Be $null
66 | }
67 |
68 | It 'Allows the object to be decrypted with the new password' {
69 | { $null = Unprotect-Data -InputObject $protected -Password $wrongPassword -ErrorAction Stop } | Should Not Throw
70 | }
71 |
72 | It 'Removes a password from the object' {
73 | { $null = Remove-ProtectedDataCredential -InputObject $protected -Password $secondPassword -ErrorAction Stop } | Should Not Throw
74 | }
75 |
76 | It 'No longer allows the data to be decrypted with the removed password' {
77 | { $null = Unprotect-Data -InputObject $protected -Password $secondPassword -ErrorAction Stop } | Should Throw
78 | }
79 | }
80 |
81 | Context 'Protecting strings' {
82 | $protectedData = $stringToEncrypt | Protect-Data -Password $passwordForEncryption -PasswordIterationCount $iterationCount
83 | $decrypted = $protectedData | Unprotect-Data -Password $passwordForEncryption
84 |
85 | It 'Does not return null' {
86 | $decrypted | Should Not Be $null
87 | }
88 |
89 | It 'Returns a String object' {
90 | $decrypted.GetType().FullName | Should Be System.String
91 | }
92 |
93 | It 'Decrypts the string properly.' {
94 | $decrypted | Should Be $stringToEncrypt
95 | }
96 | }
97 |
98 | Context 'Protecting SecureStrings' {
99 | $protectedData = $secureStringToEncrypt | Protect-Data -Password $passwordForEncryption -PasswordIterationCount $iterationCount
100 | $decrypted = $protectedData | Unprotect-Data -Password $passwordForEncryption
101 |
102 | It 'Does not return null' {
103 | $decrypted | Should Not Be $null
104 | }
105 |
106 | It 'Returns a SecureString object' {
107 | $decrypted.GetType().FullName | Should Be System.Security.SecureString
108 | }
109 |
110 | It 'Decrypts the SecureString properly.' {
111 | Get-PlainTextFromSecureString -SecureString $decrypted | Should Be $stringToEncrypt
112 | }
113 | }
114 |
115 | Context 'Protecting PSCredentials' {
116 | $protectedData = $credentialToEncrypt | Protect-Data -Password $passwordForEncryption -PasswordIterationCount $iterationCount
117 | $decrypted = $protectedData | Unprotect-Data -Password $passwordForEncryption
118 |
119 | It 'Does not return null' {
120 | $decrypted | Should Not Be $null
121 | }
122 |
123 | It 'Returns a PSCredential object' {
124 | $decrypted.GetType().FullName | Should Be System.Management.Automation.PSCredential
125 | }
126 |
127 | It 'Decrypts the PSCredential properly (username)' {
128 | $decrypted.UserName | Should Be $userName
129 | }
130 |
131 | It 'Decrypts the PSCredential properly (password)' {
132 | Get-PlainTextFromSecureString -SecureString $decrypted.Password | Should Be $stringToEncrypt
133 | }
134 | }
135 |
136 | Context 'Protecting Byte Arrays' {
137 | $protectedData = Protect-Data -InputObject $byteArrayToEncrypt -Password $passwordForEncryption -PasswordIterationCount $iterationCount
138 | $decrypted = Unprotect-Data -InputObject $protectedData -Password $passwordForEncryption
139 |
140 | It 'Does not return null' {
141 | ,$decrypted | Should Not Be $null
142 | }
143 |
144 | It 'Returns a byte array' {
145 | $decrypted.GetType().FullName | Should Be System.Byte[]
146 | }
147 |
148 | It 'Decrypts the byte array properly' {
149 | ($byteArrayToEncrypt.Length -eq $decrypted.Length -and (-join $byteArrayToEncrypt) -eq (-join $decrypted)) | Should Be $True
150 | }
151 | }
152 | }
153 |
154 | Describe 'Certificate-based encryption and decryption (By thumbprint)' {
155 | Remove-TestCertificate
156 |
157 | $path = @{}
158 |
159 | if ($PSVersionTable.PSVersion.Major -le 2)
160 | {
161 | # The certificate provider in PowerShell 2.0 is quite slow for some reason, and filtering
162 | # this test down to the store where we know the certs are located makes it less painful
163 | # to run this test.
164 | $path = @{ Path = 'Cert:\CurrentUser\My' }
165 | }
166 |
167 | $certThumbprint = New-TestCertificate -Subject $testCertificateSubject
168 | $secondCertThumbprint = New-TestCertificate -Subject $testCertificateSubject
169 | $wrongCertThumbprint = New-TestCertificate -Subject $testCertificateSubject
170 |
171 | Context 'Finding suitable certificates for encryption and decryption' {
172 | $certificates = @(
173 | Get-KeyEncryptionCertificate @path -RequirePrivateKey |
174 | Where-Object { ($certThumbprint, $secondCertThumbprint, $wrongCertThumbprint) -contains $_.Thumbprint }
175 | )
176 |
177 | It 'Find the test certificates' {
178 | $certificates.Count | Should Be 3
179 | }
180 | }
181 |
182 | Context 'General Usage' {
183 | $protected = Protect-Data -InputObject $stringToEncrypt -Certificate $certThumbprint
184 |
185 | It 'Decrypts data successfully' {
186 | Unprotect-Data -InputObject $protected -Certificate $certThumbprint -ErrorAction Stop | Should Be $stringToEncrypt
187 | }
188 | }
189 |
190 | Remove-TestCertificate
191 | }
192 |
193 | Describe 'Certificate-Based encryption and decryption (By certificate object)' {
194 | $certFromFile = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("$scriptRoot\TestCertificateFile.pfx", 'password')
195 | $SecondCertFromFile = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("$scriptRoot\TestCertificateFile2.pfx", 'password')
196 | $WrongCertFromFile = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("$scriptRoot\TestCertificateFile3.pfx", 'password')
197 |
198 | Context 'General Usage' {
199 | It 'Does not produce an error when a self-signed or otherwise invalid certificate is used.' {
200 | { $null = Protect-Data -InputObject $stringToEncrypt -Certificate $certFromFile -ErrorAction Stop } | Should Not Throw
201 | }
202 |
203 | $protected = Protect-Data -InputObject $stringToEncrypt -Certificate $certFromFile, $secondCertFromFile
204 |
205 | It 'Produces an error if a decryption attempt with the wrong certificate is made.' {
206 | { $null = Unprotect-Data -InputObject $protected -Certificate $wrongCertFromFile -ErrorAction Stop } | Should Throw
207 | }
208 |
209 | It 'Allows any of the specified certificates to be used during decryption (First certificate test)' {
210 | { $null = Unprotect-Data -InputObject $protected -Certificate $certFromFile -ErrorAction Stop } | Should Not Throw
211 | }
212 |
213 | It 'Allows any of the specified certificates to be used during decryption (Second certificate test)' {
214 | { $null = Unprotect-Data -InputObject $protected -Certificate $secondCertFromFile -ErrorAction Stop } | Should Not Throw
215 | }
216 |
217 | It 'Adds a new certificate to an existing object' {
218 | $scriptBlock = {
219 | Add-ProtectedDataCredential -InputObject $protected -Certificate $secondCertFromFile -NewCertificate $wrongCertFromFile -ErrorAction Stop
220 | }
221 |
222 | $scriptBlock | Should Not Throw
223 | }
224 |
225 | It 'Allows the object to be decrypted with the new certificate' {
226 | { $null = Unprotect-Data -InputObject $protected -Certificate $wrongCertFromFile -ErrorAction Stop } | Should Not Throw
227 | }
228 |
229 | It 'Removes a certificate from the object' {
230 | { $null = Remove-ProtectedDataCredential -InputObject $protected -Certificate $secondCertFromFile -ErrorAction Stop } | Should Not Throw
231 | }
232 |
233 | It 'No longer allows the data to be decrypted with the removed certificate' {
234 | { $null = Unprotect-Data -InputObject $protected -Certificate $secondCertFromFile -ErrorAction Stop } | Should Throw
235 | }
236 | }
237 |
238 | Context 'Protecting strings' {
239 | $protectedData = $stringToEncrypt | Protect-Data -Certificate $certFromFile
240 | $decrypted = $protectedData | Unprotect-Data -Certificate $certFromFile
241 |
242 | It 'Does not return null' {
243 | $decrypted | Should Not Be $null
244 | }
245 |
246 | It 'Returns a String object' {
247 | $decrypted.GetType().FullName | Should Be System.String
248 | }
249 |
250 | It 'Decrypts the string properly.' {
251 | $decrypted | Should Be $stringToEncrypt
252 | }
253 | }
254 |
255 | Context 'Protecting SecureStrings' {
256 | $protectedData = $secureStringToEncrypt | Protect-Data -Certificate $certFromFile
257 | $decrypted = $protectedData | Unprotect-Data -Certificate $certFromFile
258 |
259 | It 'Does not return null' {
260 | $decrypted | Should Not Be $null
261 | }
262 |
263 | It 'Returns a SecureString object' {
264 | $decrypted.GetType().FullName | Should Be System.Security.SecureString
265 | }
266 |
267 | It 'Decrypts the SecureString properly.' {
268 | Get-PlainTextFromSecureString -SecureString $decrypted | Should Be $stringToEncrypt
269 | }
270 | }
271 |
272 | Context 'Protecting PSCredentials' {
273 | $protectedData = $credentialToEncrypt | Protect-Data -Certificate $certFromFile
274 | $decrypted = $protectedData | Unprotect-Data -Certificate $certFromFile
275 |
276 | It 'Does not return null' {
277 | $decrypted | Should Not Be $null
278 | }
279 |
280 | It 'Returns a PSCredential object' {
281 | $decrypted.GetType().FullName | Should Be System.Management.Automation.PSCredential
282 | }
283 |
284 | It 'Decrypts the PSCredential properly (username)' {
285 | $decrypted.UserName | Should Be $userName
286 | }
287 |
288 | It 'Decrypts the PSCredential properly (password)' {
289 | Get-PlainTextFromSecureString -SecureString $decrypted.Password | Should Be $stringToEncrypt
290 | }
291 | }
292 |
293 | Context 'Protecting Byte Arrays' {
294 | $protectedData = Protect-Data -InputObject $byteArrayToEncrypt -Certificate $certFromFile
295 | $decrypted = Unprotect-Data -InputObject $protectedData -Certificate $certFromFile
296 |
297 | It 'Does not return null' {
298 | ,$decrypted | Should Not Be $null
299 | }
300 |
301 | It 'Returns a byte array' {
302 | $decrypted.GetType().FullName | Should Be System.Byte[]
303 | }
304 |
305 | It 'Decrypts the byte array properly' {
306 | ($byteArrayToEncrypt.Length -eq $decrypted.Length -and (-join $byteArrayToEncrypt) -eq (-join $decrypted)) | Should Be $True
307 | }
308 | }
309 | }
310 |
311 | Describe 'Certificate-based encryption / decryption (by file system path)' {
312 | $certFromFile = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("$scriptRoot\TestCertificateFile.pfx", 'password')
313 |
314 | $hash = @{ ProtectedData = $null }
315 |
316 | It 'Encrypts data successfully with a relative filesystem path to a certificate file' {
317 | [System.Environment]::CurrentDirectory = $env:temp
318 | Push-Location $scriptRoot
319 |
320 | try
321 | {
322 | { $hash.ProtectedData = Protect-Data $stringToEncrypt -Certificate '.\TestCertificateFile.cer' -ErrorAction Stop } |
323 | Should Not Throw
324 | }
325 | finally
326 | {
327 | Pop-Location
328 | }
329 | }
330 |
331 | It 'Decrypts the data successfully' {
332 | Unprotect-Data -InputObject $hash.ProtectedData -Certificate $certFromFile |
333 | Should Be $stringToEncrypt
334 | }
335 | }
336 |
337 | Describe 'Certificate-based encryption / decryption (by certificate path)' {
338 | $testThumbprint = New-TestCertificate -Subject $testCertificateSubject
339 |
340 | $hash = @{}
341 |
342 | It 'Encrypts data successfully with a relative certificate provider path' {
343 | Push-Location Cert:\CurrentUser\My
344 |
345 | try
346 | {
347 | { $hash.ProtectedData = Protect-Data $stringToEncrypt -Certificate ".\$testThumbprint" -ErrorAction Stop } |
348 | Should Not Throw
349 | }
350 | finally
351 | {
352 | Pop-Location
353 | }
354 | }
355 |
356 | It 'Decrypts the data successfully' {
357 | Push-Location Cert:\CurrentUser\My
358 |
359 | try
360 | {
361 | Unprotect-Data -InputObject $hash.ProtectedData -Certificate ".\$testThumbprint" |
362 | Should Be $stringToEncrypt
363 | }
364 | finally
365 | {
366 | Pop-Location
367 | }
368 | }
369 | }
370 |
371 | Describe 'Certificate-based decryption (automatic detection of cert)' {
372 | Remove-TestCertificate
373 |
374 | $certThumbprint = New-TestCertificate -Subject $testCertificateSubject
375 |
376 | $protectedData = $stringToEncrypt | Protect-Data -Certificate Cert:\CurrentUser\My\$certThumbprint
377 |
378 | It 'Successfully finds the matching certificate and decrypts the data' {
379 | $hash = @{}
380 | $scriptBlock = { $hash.Decrypted = Unprotect-Data $protectedData -ErrorAction Stop }
381 |
382 | $scriptBlock | Should Not Throw
383 | $hash.Decrypted | Should Be $stringToEncrypt
384 | }
385 |
386 | Remove-TestCertificate
387 |
388 | It 'Gives a useful error message when no matching certificate is found' {
389 | $scriptBlock = { $null = Unprotect-Data $protectedData -ErrorAction Stop }
390 | $scriptBlock | Should Throw 'No decryption certificate for the specified InputObject was found'
391 | }
392 | }
393 |
394 | Describe 'HMAC authentication of AES data' {
395 | $protectedData = $stringToEncrypt | Protect-Data -Password $passwordForEncryption
396 |
397 | $cipherText = $protectedData.CipherText.Clone()
398 |
399 | It 'Throws an error if the ciphertext has been modified' {
400 | $protectedData.CipherText[0] = ($protectedData.CipherText[0] + 12) % 256
401 | { $protectedData | Unprotect-Data -Password $passwordForEncryption -ErrorAction Stop } | Should Throw 'Decryption failed due to invalid HMAC.'
402 | }
403 |
404 | $protectedData.CipherText = $cipherText.Clone()
405 |
406 | It 'Throws an error if the HMAC has been modified' {
407 | $protectedData.HMAC[0] = ($protectedData.HMAC[0] + 12) % 256
408 | { $protectedData | Unprotect-Data -Password $passwordForEncryption -ErrorAction Stop } | Should Throw 'Decryption failed due to invalid HMAC.'
409 | }
410 | }
411 |
412 | Describe 'Legacy Padding Support' {
413 | $certFromFile = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("$scriptRoot\TestCertificateFile.pfx", 'password')
414 | $stringToEncrypt = 'This is a test'
415 |
416 | Context 'Loading protected data from previous version of module' {
417 | $protectedData = Import-Clixml -Path $scriptRoot\V1.0.ProtectedWithTestCertificateFile.pfx.xml
418 | Set-StrictMode -Version Latest
419 |
420 | It 'Throws an error if the HMAC code is missing' {
421 | $scriptBlock = { $protectedData | Unprotect-Data -Certificate $certFromFile -ErrorAction Stop }
422 | $scriptBlock | Should Throw 'Input Object contained no HMAC code'
423 | }
424 |
425 | It 'Adds an HMAC to the legacy object' {
426 | $protectedData | Add-ProtectedDataHmac -Certificate $certFromFile
427 | ,$protectedData.HMAC | Should Not BeNullOrEmpty
428 | }
429 |
430 | It 'Unprotects the data properly even with strict mode enabled' {
431 | { $protectedData | Unprotect-Data -Certificate $certFromFile -ErrorAction Stop } | Should Not Throw
432 | $protectedData | Unprotect-Data -Certificate $certFromFile | Should Be $stringToEncrypt
433 | }
434 | }
435 |
436 | Context 'Using legacy padding' {
437 | $protectedData = Protect-Data -InputObject $stringToEncrypt -Certificate $certFromFile -UseLegacyPadding
438 |
439 | It 'Assigns the use legacy padding property' {
440 | $protectedData.KeyData[0].LegacyPadding | Should Be $true
441 | }
442 |
443 | It 'Decrypts data properly' {
444 | $protectedData |
445 | Unprotect-Data -Certificate $certFromFile |
446 | Should Be $stringToEncrypt
447 | }
448 | }
449 |
450 | Context 'Using OAEP padding' {
451 | $protectedData = Protect-Data -InputObject $stringToEncrypt -Certificate $certFromFile
452 |
453 | It 'Does not assign the use legacy padding property' {
454 | $protectedData.KeyData[0].LegacyPadding | Should Be $false
455 | }
456 |
457 | It 'Decrypts data properly' {
458 | $protectedData |
459 | Unprotect-Data -Certificate $certFromFile |
460 | Should Be $stringToEncrypt
461 | }
462 | }
463 | }
464 |
465 | Describe 'RSA Certificates (CNG Key Storage Provider)' {
466 | Context 'RSA Certificates (CNG Key Storage Provider)' {
467 | $thumbprint = New-TestCertificate -Subject $testCertificateSubject -CertificateType RsaCng
468 | $testCert = Get-Item Cert:\CurrentUser\My\$thumbprint
469 |
470 | $protectedData = Protect-Data -InputObject $stringToEncrypt -Certificate $testCert
471 |
472 | It 'Decrypts data successfully using an RSA cert using a CNG KSP' {
473 | Unprotect-Data -InputObject $protectedData -Certificate $testCert |
474 | Should Be $stringToEncrypt
475 | }
476 |
477 | $protectedWithLegacyPadding = Protect-Data -InputObject $stringToEncrypt -Certificate $testCert -UseLegacyPadding
478 |
479 | It 'Decrypts data successfully with legacy padding' {
480 | Unprotect-Data -InputObject $protectedWithLegacyPadding -Certificate $testCert |
481 | Should Be $stringToEncrypt
482 | }
483 | }
484 |
485 | Remove-TestCertificate
486 | }
487 |
488 | Describe 'ECDH Certificates' {
489 | Context 'ECDH_P256' {
490 | $thumbprint = New-TestCertificate -Subject $testCertificateSubject -CertificateType Ecdh_P256
491 | $testCert = Get-Item Cert:\CurrentUser\My\$thumbprint
492 |
493 | $protectedData = Protect-Data -InputObject $stringToEncrypt -Certificate $testCert
494 |
495 | It 'Decrypts data successfully using an ECDH_P256 certificate' {
496 | Unprotect-Data -InputObject $protectedData -Certificate $testCert |
497 | Should Be $stringToEncrypt
498 | }
499 | }
500 |
501 | Context 'ECDH_P384' {
502 | $thumbprint = New-TestCertificate -Subject $testCertificateSubject -CertificateType Ecdh_P384
503 | $testCert = Get-Item Cert:\CurrentUser\My\$thumbprint
504 |
505 | $protectedData = Protect-Data -InputObject $stringToEncrypt -Certificate $testCert
506 |
507 | It 'Decrypts data successfully using an ECDH_P384 certificate' {
508 | Unprotect-Data -InputObject $protectedData -Certificate $testCert |
509 | Should Be $stringToEncrypt
510 | }
511 | }
512 |
513 | Context 'ECDH_P521' {
514 | $thumbprint = New-TestCertificate -Subject $testCertificateSubject -CertificateType Ecdh_P521
515 | $testCert = Get-Item Cert:\CurrentUser\My\$thumbprint
516 |
517 | $protectedData = Protect-Data -InputObject $stringToEncrypt -Certificate $testCert
518 |
519 | It 'Decrypts data successfully using an ECDH_P521 certificate' {
520 | Unprotect-Data -InputObject $protectedData -Certificate $testCert |
521 | Should Be $stringToEncrypt
522 | }
523 | }
524 |
525 | Remove-TestCertificate
526 | }
527 |
--------------------------------------------------------------------------------
/Commands.ps1:
--------------------------------------------------------------------------------
1 | if ($PSVersionTable.PSVersion.Major -eq 2)
2 | {
3 | $IgnoreError = 'SilentlyContinue'
4 | }
5 | else
6 | {
7 | $IgnoreError = 'Ignore'
8 | }
9 |
10 | $script:ValidTypes = @(
11 | [string]
12 | [System.Security.SecureString]
13 | [System.Management.Automation.PSCredential]
14 | [byte[]]
15 | )
16 |
17 | $script:PSCredentialHeader = [byte[]](5,12,19,75,80,20,19,11,11,6,11,13)
18 |
19 | $script:EccAlgorithmOid = '1.2.840.10045.2.1'
20 |
21 | #region Exported functions
22 |
23 | function Protect-Data
24 | {
25 | <#
26 | .Synopsis
27 | Encrypts an object using one or more digital certificates and/or passwords.
28 | .DESCRIPTION
29 | Encrypts an object using a randomly-generated AES key. AES key information is encrypted using one or more certificate public keys and/or password-derived keys, allowing the data to be securely shared among multiple users and computers.
30 | If certificates are used, they must be installed in either the local computer or local user's certificate stores, and the certificates' Key Usage extension must allow Key Encipherment (for RSA) or Key Agreement (for ECDH). The private keys are not required for Protect-Data.
31 | .PARAMETER InputObject
32 | The object that is to be encrypted. The object must be of one of the types returned by the Get-ProtectedDataSupportedTypes command.
33 | .PARAMETER Certificate
34 | Zero or more RSA or ECDH certificates that should be used to encrypt the data. The data can later be decrypted by using the same certificate (with its private key.) You can pass an X509Certificate2 object to this parameter, or you can pass in a string which contains either a path to a certificate file on the file system, a path to the certificate in the Certificate provider, or a certificate thumbprint (in which case the certificate provider will be searched to find the certificate.)
35 | .PARAMETER UseLegacyPadding
36 | Optional switch specifying that when performing certificate-based encryption, PKCS#1 v1.5 padding should be used instead of the newer, more secure OAEP padding scheme. Some certificates may not work properly with OAEP padding
37 | .PARAMETER Password
38 | Zero or more SecureString objects containing password that will be used to derive encryption keys. The data can later be decrypted by passing in a SecureString with the same value.
39 | .PARAMETER SkipCertificateVerification
40 | Deprecated parameter, which will be removed in a future release. Specifying this switch will generate a warning.
41 | .PARAMETER PasswordIterationCount
42 | Optional positive integer value specifying the number of iteration that should be used when deriving encryption keys from the specified password(s). Defaults to 50000.
43 | Higher values make it more costly to crack the passwords by brute force.
44 | .EXAMPLE
45 | $encryptedObject = Protect-Data -InputObject $myString -CertificateThumbprint CB04E7C885BEAE441B39BC843C85855D97785D25 -Password (Read-Host -AsSecureString -Prompt 'Enter password to encrypt')
46 |
47 | Encrypts a string using a single RSA or ECDH certificate, and a password. Either the certificate or the password can be used when decrypting the data.
48 | .EXAMPLE
49 | $credential | Protect-Data -CertificateThumbprint 'CB04E7C885BEAE441B39BC843C85855D97785D25', 'B5A04AB031C24BCEE220D6F9F99B6F5D376753FB'
50 |
51 | Encrypts a PSCredential object using two RSA or ECDH certificates. Either private key can be used to later decrypt the data.
52 | .INPUTS
53 | Object
54 |
55 | Object must be one of the types returned by the Get-ProtectedDataSupportedTypes command.
56 | .OUTPUTS
57 | PSObject
58 |
59 | The output object contains the following properties:
60 |
61 | CipherText : An array of bytes containing the encrypted data
62 | Type : A string representation of the InputObject's original type (used when decrypting back to the original object later.)
63 | KeyData : One or more structures which contain encrypted copies of the AES key used to protect the ciphertext, and other identifying information about the way this copy of the keys was protected, such as Certificate Thumbprint, Password Hash, Salt values, and Iteration count.
64 | .LINK
65 | Unprotect-Data
66 | .LINK
67 | Add-ProtectedDataCredential
68 | .LINK
69 | Remove-ProtectedDataCredential
70 | .LINK
71 | Get-ProtectedDataSupportedTypes
72 | #>
73 |
74 | [CmdletBinding()]
75 | [OutputType([psobject])]
76 | param (
77 | [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
78 | [ValidateScript({
79 | if ($script:ValidTypes -notcontains $_.GetType() -and $null -eq ($_ -as [byte[]]))
80 | {
81 | throw "InputObject must be one of the following types: $($script:ValidTypes -join ', ')"
82 | }
83 |
84 | if ($_ -is [System.Security.SecureString] -and $_.Length -eq 0)
85 | {
86 | throw 'SecureString argument contained no data.'
87 | }
88 |
89 | return $true
90 | })]
91 | $InputObject,
92 |
93 | [ValidateNotNullOrEmpty()]
94 | [AllowEmptyCollection()]
95 | [object[]]
96 | $Certificate = @(),
97 |
98 | [switch]
99 | $UseLegacyPadding,
100 |
101 | [ValidateNotNull()]
102 | [AllowEmptyCollection()]
103 | [ValidateScript({
104 | if ($_.Length -eq 0)
105 | {
106 | throw 'You may not pass empty SecureStrings to the Password parameter'
107 | }
108 |
109 | return $true
110 | })]
111 | [System.Security.SecureString[]]
112 | $Password = @(),
113 |
114 | [ValidateRange(1,2147483647)]
115 | [int]
116 | $PasswordIterationCount = 50000,
117 |
118 | [switch]
119 | $SkipCertificateVerification
120 | )
121 |
122 | begin
123 | {
124 | if ($PSBoundParameters.ContainsKey('SkipCertificateVerification'))
125 | {
126 | Write-Warning 'The -SkipCertificateVerification switch has been deprecated, and the module now treats that as its default behavior. This switch will be removed in a future release.'
127 | }
128 |
129 | $certs = @(
130 | foreach ($cert in $Certificate)
131 | {
132 | try
133 | {
134 |
135 | $x509Cert = ConvertTo-X509Certificate2 -InputObject $cert -ErrorAction Stop
136 | ValidateKeyEncryptionCertificate -CertificateGroup $x509Cert -ErrorAction Stop
137 | }
138 | catch
139 | {
140 | Write-Error -ErrorRecord $_
141 | }
142 | }
143 | )
144 |
145 | if ($certs.Count -eq 0 -and $Password.Count -eq 0)
146 | {
147 | throw ('None of the specified certificates could be used for encryption, and no passwords were specified.' +
148 | ' Data protection cannot be performed.')
149 | }
150 | }
151 |
152 | process
153 | {
154 | $plainText = $null
155 | $payload = $null
156 |
157 | try
158 | {
159 | $plainText = ConvertTo-PinnedByteArray -InputObject $InputObject
160 | $payload = Protect-DataWithAes -PlainText $plainText
161 |
162 | $protectedData = New-Object psobject -Property @{
163 | CipherText = $payload.CipherText
164 | HMAC = $payload.HMAC
165 | Type = $InputObject.GetType().FullName
166 | KeyData = @()
167 | }
168 |
169 | $params = @{
170 | InputObject = $protectedData
171 | Key = $payload.Key
172 | InitializationVector = $payload.IV
173 | Certificate = $certs
174 | Password = $Password
175 | PasswordIterationCount = $PasswordIterationCount
176 | UseLegacyPadding = $UseLegacyPadding
177 | }
178 |
179 | Add-KeyData @params
180 |
181 | if ($protectedData.KeyData.Count -eq 0)
182 | {
183 | Write-Error 'Failed to protect data with any of the supplied certificates or passwords.'
184 | return
185 | }
186 | else
187 | {
188 | $protectedData
189 | }
190 | }
191 | finally
192 | {
193 | if ($plainText -is [IDisposable]) { $plainText.Dispose() }
194 | if ($null -ne $payload)
195 | {
196 | if ($payload.Key -is [IDisposable]) { $payload.Key.Dispose() }
197 | if ($payload.IV -is [IDisposable]) { $payload.IV.Dispose() }
198 | }
199 | }
200 |
201 | } # process
202 |
203 | } # function Protect-Data
204 |
205 | function Unprotect-Data
206 | {
207 | <#
208 | .Synopsis
209 | Decrypts an object that was produced by the Protect-Data command.
210 | .DESCRIPTION
211 | Decrypts an object that was produced by the Protect-Data command. If a Certificate is used to perform the decryption, it must be installed in either the local computer or current user's certificate stores (with its private key), and the current user must have permission to use that key.
212 | .PARAMETER InputObject
213 | The ProtectedData object that is to be decrypted.
214 | .PARAMETER Certificate
215 | An RSA or ECDH certificate that will be used to decrypt the data. You must have the certificate's private key, and it must be one of the certificates that was used to encrypt the data. You can pass an X509Certificate2 object to this parameter, or you can pass in a string which contains either a path to a certificate file on the file system, a path to the certificate in the Certificate provider, or a certificate thumbprint (in which case the certificate provider will be searched to find the certificate.)
216 | .PARAMETER Password
217 | A SecureString containing a password that will be used to derive an encryption key. One of the InputObject's KeyData objects must be protected with this password.
218 | .PARAMETER SkipCertificateValidation
219 | Deprecated parameter, which will be removed in a future release. Specifying this switch will generate a warning.
220 | .EXAMPLE
221 | $decryptedObject = $encryptedObject | Unprotect-Data -Password (Read-Host -AsSecureString -Prompt 'Enter password to decrypt the data')
222 |
223 | Decrypts the contents of $encryptedObject and outputs an object of the same type as what was originally passed to Protect-Data. Uses a password to decrypt the object instead of a certificate.
224 | .INPUTS
225 | PSObject
226 |
227 | The input object should be a copy of an object that was produced by Protect-Data.
228 | .OUTPUTS
229 | Object
230 |
231 | Object may be any type returned by Get-ProtectedDataSupportedTypes. Specifically, it will be an object of the type specified in the InputObject's Type property.
232 | .LINK
233 | Protect-Data
234 | .LINK
235 | Add-ProtectedDataCredential
236 | .LINK
237 | Remove-ProtectedDataCredential
238 | .LINK
239 | Get-ProtectedDataSupportedTypes
240 | #>
241 |
242 | [CmdletBinding(DefaultParameterSetName = 'Certificate')]
243 | param (
244 | [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
245 | [ValidateScript({
246 | if (-not (Test-IsProtectedData -InputObject $_))
247 | {
248 | throw 'InputObject argument must be a ProtectedData object.'
249 | }
250 |
251 | if ($null -eq $_.CipherText -or $_.CipherText.Count -eq 0)
252 | {
253 | throw 'Protected data object contained no cipher text.'
254 | }
255 |
256 | $type = $_.Type -as [type]
257 |
258 | if ($null -eq $type -or $script:ValidTypes -notcontains $type)
259 | {
260 | throw 'Protected data object specified an invalid type. Type must be one of: ' +
261 | ($script:ValidTypes -join ', ')
262 | }
263 |
264 | return $true
265 | })]
266 | $InputObject,
267 |
268 | [Parameter(ParameterSetName = 'Certificate')]
269 | [object]
270 | $Certificate,
271 |
272 | [Parameter(Mandatory = $true, ParameterSetName = 'Password')]
273 | [System.Security.SecureString]
274 | $Password,
275 |
276 | [switch]
277 | $SkipCertificateVerification
278 | )
279 |
280 | begin
281 | {
282 | if ($PSBoundParameters.ContainsKey('SkipCertificateVerification'))
283 | {
284 | Write-Warning 'The -SkipCertificateVerification switch has been deprecated, and the module now treats that as its default behavior. This switch will be removed in a future release.'
285 | }
286 |
287 | $cert = $null
288 |
289 | if ($Certificate)
290 | {
291 | try
292 | {
293 | $cert = ConvertTo-X509Certificate2 -InputObject $Certificate -ErrorAction Stop
294 |
295 | $params = @{
296 | CertificateGroup = $cert
297 | RequirePrivateKey = $true
298 | }
299 |
300 | $cert = ValidateKeyEncryptionCertificate @params -ErrorAction Stop
301 | }
302 | catch
303 | {
304 | throw
305 | }
306 | }
307 | }
308 |
309 | process
310 | {
311 | $plainText = $null
312 | $aes = $null
313 | $key = $null
314 | $iv = $null
315 |
316 | if ($null -ne $Password)
317 | {
318 | $params = @{ Password = $Password }
319 | }
320 | else
321 | {
322 | if ($null -eq $cert)
323 | {
324 | $paths = 'Cert:\CurrentUser\My', 'Cert:\LocalMachine\My'
325 |
326 | $cert = :outer foreach ($path in $paths)
327 | {
328 | foreach ($keyData in $InputObject.KeyData)
329 | {
330 | if ($keyData.Thumbprint)
331 | {
332 | $certObject = $null
333 | try
334 | {
335 | $certObject = Get-KeyEncryptionCertificate -Path $path -CertificateThumbprint $keyData.Thumbprint -RequirePrivateKey -ErrorAction $IgnoreError
336 | } catch { }
337 |
338 | if ($null -ne $certObject)
339 | {
340 | $certObject
341 | break outer
342 | }
343 | }
344 | }
345 | }
346 | }
347 |
348 | if ($null -eq $cert)
349 | {
350 | Write-Error -Message 'No decryption certificate for the specified InputObject was found.' -TargetObject $InputObject
351 | return
352 | }
353 |
354 | $params = @{
355 | Certificate = $cert
356 | }
357 | }
358 |
359 | try
360 | {
361 | $result = Unprotect-MatchingKeyData -InputObject $InputObject @params
362 | $key = $result.Key
363 | $iv = $result.IV
364 |
365 | if ($null -eq $InputObject.HMAC)
366 | {
367 | throw 'Input Object contained no HMAC code.'
368 | }
369 |
370 | $hmac = $InputObject.HMAC
371 |
372 | $plainText = (Unprotect-DataWithAes -CipherText $InputObject.CipherText -Key $key -InitializationVector $iv -HMAC $hmac).PlainText
373 |
374 | ConvertFrom-ByteArray -ByteArray $plainText -Type $InputObject.Type -ByteCount $plainText.Count
375 | }
376 | catch
377 | {
378 | Write-Error -ErrorRecord $_
379 | return
380 | }
381 | finally
382 | {
383 | if ($plainText -is [IDisposable]) { $plainText.Dispose() }
384 | if ($key -is [IDisposable]) { $key.Dispose() }
385 | if ($iv -is [IDisposable]) { $iv.Dispose() }
386 | }
387 |
388 | } # process
389 |
390 | } # function Unprotect-Data
391 |
392 | function Add-ProtectedDataHmac
393 | {
394 | <#
395 | .Synopsis
396 | Adds an HMAC authentication code to a ProtectedData object which was created with a previous version of the module.
397 | .DESCRIPTION
398 | Adds an HMAC authentication code to a ProtectedData object which was created with a previous version of the module. The parameters and requirements are the same as for the Unprotect-Data command, as the data must be partially decrypted in order to produce the HMAC code.
399 | .PARAMETER InputObject
400 | The ProtectedData object that is to have an HMAC generated.
401 | .PARAMETER Certificate
402 | An RSA or ECDH certificate that will be used to decrypt the data. You must have the certificate's private key, and it must be one of the certificates that was used to encrypt the data. You can pass an X509Certificate2 object to this parameter, or you can pass in a string which contains either a path to a certificate file on the file system, a path to the certificate in the Certificate provider, or a certificate thumbprint (in which case the certificate provider will be searched to find the certificate.)
403 | .PARAMETER Password
404 | A SecureString containing a password that will be used to derive an encryption key. One of the InputObject's KeyData objects must be protected with this password.
405 | .PARAMETER SkipCertificateVerification
406 | Deprecated parameter, which will be removed in a future release. Specifying this switch will generate a warning.
407 | .PARAMETER PassThru
408 | If specified, the command outputs the ProtectedData object after adding the HMAC.
409 | .EXAMPLE
410 | $encryptedObject | Add-ProtectedDataHmac -Password (Read-Host -AsSecureString -Prompt 'Enter password to decrypt the key data')
411 |
412 | Adds an HMAC code to the $encryptedObject object.
413 | .INPUTS
414 | PSObject
415 |
416 | The input object should be a copy of an object that was produced by Protect-Data.
417 | .OUTPUTS
418 | None, or ProtectedData object if the -PassThru switch is used.
419 | .LINK
420 | Protect-Data
421 | .LINK
422 | Unprotect-Data
423 | .LINK
424 | Add-ProtectedDataCredential
425 | .LINK
426 | Remove-ProtectedDataCredential
427 | .LINK
428 | Get-ProtectedDataSupportedTypes
429 | #>
430 |
431 | [CmdletBinding(DefaultParameterSetName = 'Certificate')]
432 | param (
433 | [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
434 | [ValidateScript({
435 | if (-not (Test-IsProtectedData -InputObject $_))
436 | {
437 | throw 'InputObject argument must be a ProtectedData object.'
438 | }
439 |
440 | if ($null -eq $_.CipherText -or $_.CipherText.Count -eq 0)
441 | {
442 | throw 'Protected data object contained no cipher text.'
443 | }
444 |
445 | $type = $_.Type -as [type]
446 |
447 | if ($null -eq $type -or $script:ValidTypes -notcontains $type)
448 | {
449 | throw 'Protected data object specified an invalid type. Type must be one of: ' +
450 | ($script:ValidTypes -join ', ')
451 | }
452 |
453 | return $true
454 | })]
455 | $InputObject,
456 |
457 | [Parameter(Mandatory = $true, ParameterSetName = 'Certificate')]
458 | [object]
459 | $Certificate,
460 |
461 | [Parameter(Mandatory = $true, ParameterSetName = 'Password')]
462 | [System.Security.SecureString]
463 | $Password,
464 |
465 | [switch]
466 | $SkipCertificateVerification,
467 |
468 | [switch]
469 | $PassThru
470 | )
471 |
472 | begin
473 | {
474 | if ($PSBoundParameters.ContainsKey('SkipCertificateVerification'))
475 | {
476 | Write-Warning 'The -SkipCertificateVerification switch has been deprecated, and the module now treats that as its default behavior. This switch will be removed in a future release.'
477 | }
478 |
479 | $cert = $null
480 |
481 | if ($Certificate)
482 | {
483 | try
484 | {
485 | $cert = ConvertTo-X509Certificate2 -InputObject $Certificate -ErrorAction Stop
486 |
487 | $params = @{
488 | CertificateGroup = $cert
489 | RequirePrivateKey = $true
490 | }
491 |
492 | $cert = ValidateKeyEncryptionCertificate @params -ErrorAction Stop
493 | }
494 | catch
495 | {
496 | throw
497 | }
498 | }
499 | }
500 |
501 | process
502 | {
503 | $key = $null
504 | $iv = $null
505 |
506 | if ($null -ne $cert)
507 | {
508 | $params = @{ Certificate = $cert }
509 | }
510 | else
511 | {
512 | $params = @{ Password = $Password }
513 | }
514 |
515 | try
516 | {
517 | $result = Unprotect-MatchingKeyData -InputObject $InputObject @params
518 | $key = $result.Key
519 | $iv = $result.IV
520 |
521 | $hmac = Get-Hmac -Key $key -Bytes $InputObject.CipherText
522 |
523 | if ($InputObject.PSObject.Properties['HMAC'])
524 | {
525 | $InputObject.HMAC = $hmac
526 | }
527 | else
528 | {
529 | Add-Member -InputObject $InputObject -Name HMAC -Value $hmac -MemberType NoteProperty
530 | }
531 |
532 | if ($PassThru)
533 | {
534 | $InputObject
535 | }
536 | }
537 | catch
538 | {
539 | Write-Error -ErrorRecord $_
540 | return
541 | }
542 | finally
543 | {
544 | if ($key -is [IDisposable]) { $key.Dispose() }
545 | if ($iv -is [IDisposable]) { $iv.Dispose() }
546 | }
547 |
548 | } # process
549 |
550 | } # function Unprotect-Data
551 |
552 | function Add-ProtectedDataCredential
553 | {
554 | <#
555 | .Synopsis
556 | Adds one or more new copies of an encryption key to an object generated by Protect-Data.
557 | .DESCRIPTION
558 | This command can be used to add new certificates and/or passwords to an object that was previously encrypted by Protect-Data. The caller must provide one of the certificates or passwords that already exists in the ProtectedData object to perform this operation.
559 | .PARAMETER InputObject
560 | The ProtectedData object which was created by an earlier call to Protect-Data.
561 | .PARAMETER Certificate
562 | An RSA or ECDH certificate which was previously used to encrypt the ProtectedData structure's key.
563 | .PARAMETER Password
564 | A password which was previously used to encrypt the ProtectedData structure's key.
565 | .PARAMETER NewCertificate
566 | Zero or more RSA or ECDH certificates that should be used to encrypt the data. The data can later be decrypted by using the same certificate (with its private key.) You can pass an X509Certificate2 object to this parameter, or you can pass in a string which contains either a path to a certificate file on the file system, a path to the certificate in the Certificate provider, or a certificate thumbprint (in which case the certificate provider will be searched to find the certificate.)
567 | .PARAMETER UseLegacyPadding
568 | Optional switch specifying that when performing certificate-based encryption, PKCS#1 v1.5 padding should be used instead of the newer, more secure OAEP padding scheme. Some certificates may not work properly with OAEP padding
569 | .PARAMETER NewPassword
570 | Zero or more SecureString objects containing password that will be used to derive encryption keys. The data can later be decrypted by passing in a SecureString with the same value.
571 | .PARAMETER SkipCertificateVerification
572 | Deprecated parameter, which will be removed in a future release. Specifying this switch will generate a warning.
573 | .PARAMETER PasswordIterationCount
574 | Optional positive integer value specifying the number of iteration that should be used when deriving encryption keys from the specified password(s). Defaults to 50000.
575 | Higher values make it more costly to crack the passwords by brute force.
576 | .PARAMETER Passthru
577 | If this switch is used, the ProtectedData object is output to the pipeline after it is modified.
578 | .EXAMPLE
579 | Add-ProtectedDataCredential -InputObject $protectedData -Certificate $oldThumbprint -NewCertificate $newThumbprints -NewPassword $newPasswords
580 |
581 | Uses the certificate with thumbprint $oldThumbprint to add new key copies to the $protectedData object. $newThumbprints would be a string array containing thumbprints, and $newPasswords would be an array of SecureString objects.
582 | .INPUTS
583 | [PSObject]
584 |
585 | The input object should be a copy of an object that was produced by Protect-Data.
586 | .OUTPUTS
587 | None, or
588 | [PSObject]
589 | .LINK
590 | Unprotect-Data
591 | .LINK
592 | Add-ProtectedDataCredential
593 | .LINK
594 | Remove-ProtectedDataCredential
595 | .LINK
596 | Get-ProtectedDataSupportedTypes
597 | #>
598 |
599 | [CmdletBinding(DefaultParameterSetName = 'Certificate')]
600 | param (
601 | [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
602 | [ValidateScript({
603 | if (-not (Test-IsProtectedData -InputObject $_))
604 | {
605 | throw 'InputObject argument must be a ProtectedData object.'
606 | }
607 |
608 | return $true
609 | })]
610 | $InputObject,
611 |
612 | [Parameter(Mandatory = $true, ParameterSetName = 'Certificate')]
613 | [object]
614 | $Certificate,
615 |
616 | [Parameter(ParameterSetName = 'Certificate')]
617 | [switch]
618 | $UseLegacyPaddingForDecryption,
619 |
620 | [Parameter(Mandatory = $true, ParameterSetName = 'Password')]
621 | [System.Security.SecureString]
622 | $Password,
623 |
624 | [ValidateNotNull()]
625 | [AllowEmptyCollection()]
626 | [object[]]
627 | $NewCertificate = @(),
628 |
629 | [switch]
630 | $UseLegacyPadding,
631 |
632 | [ValidateNotNull()]
633 | [AllowEmptyCollection()]
634 | [System.Security.SecureString[]]
635 | $NewPassword = @(),
636 |
637 | [ValidateRange(1,2147483647)]
638 | [int]
639 | $PasswordIterationCount = 50000,
640 |
641 | [switch]
642 | $SkipCertificateVerification,
643 |
644 | [switch]
645 | $Passthru
646 | )
647 |
648 | begin
649 | {
650 | if ($PSBoundParameters.ContainsKey('SkipCertificateVerification'))
651 | {
652 | Write-Warning 'The -SkipCertificateVerification switch has been deprecated, and the module now treats that as its default behavior. This switch will be removed in a future release.'
653 | }
654 |
655 | $decryptionCert = $null
656 |
657 | if ($PSCmdlet.ParameterSetName -eq 'Certificate')
658 | {
659 | try
660 | {
661 | $decryptionCert = ConvertTo-X509Certificate2 -InputObject $Certificate -ErrorAction Stop
662 |
663 | $params = @{
664 | CertificateGroup = $decryptionCert
665 | RequirePrivateKey = $true
666 | }
667 |
668 | $decryptionCert = ValidateKeyEncryptionCertificate @params -ErrorAction Stop
669 | }
670 | catch
671 | {
672 | throw
673 | }
674 | }
675 |
676 | $certs = @(
677 | foreach ($cert in $NewCertificate)
678 | {
679 | try
680 | {
681 | $x509Cert = ConvertTo-X509Certificate2 -InputObject $cert -ErrorAction Stop
682 | ValidateKeyEncryptionCertificate -CertificateGroup $x509Cert -ErrorAction Stop
683 | }
684 | catch
685 | {
686 | Write-Error -ErrorRecord $_
687 | }
688 | }
689 | )
690 |
691 | if ($certs.Count -eq 0 -and $NewPassword.Count -eq 0)
692 | {
693 | throw 'None of the specified certificates could be used for encryption, and no passwords were ' +
694 | 'specified. Data protection cannot be performed.'
695 | }
696 |
697 | } # begin
698 |
699 | process
700 | {
701 | if ($null -ne $decryptionCert)
702 | {
703 | $params = @{ Certificate = $decryptionCert }
704 | }
705 | else
706 | {
707 | $params = @{ Password = $Password }
708 | }
709 |
710 | $key = $null
711 | $iv = $null
712 |
713 | try
714 | {
715 | $result = Unprotect-MatchingKeyData -InputObject $InputObject @params
716 | $key = $result.Key
717 | $iv = $result.IV
718 |
719 | Add-KeyData -InputObject $InputObject -Key $key -InitializationVector $iv -Certificate $certs -Password $NewPassword -PasswordIterationCount $PasswordIterationCount -UseLegacyPadding:$UseLegacyPadding
720 | }
721 | catch
722 | {
723 | Write-Error -ErrorRecord $_
724 | return
725 | }
726 | finally
727 | {
728 | if ($key -is [IDisposable]) { $key.Dispose() }
729 | if ($iv -is [IDisposable]) { $iv.Dispose() }
730 | }
731 |
732 | if ($Passthru)
733 | {
734 | $InputObject
735 | }
736 |
737 | } # process
738 |
739 | } # function Add-ProtectedDataCredential
740 |
741 | function Remove-ProtectedDataCredential
742 | {
743 | <#
744 | .Synopsis
745 | Removes copies of encryption keys from a ProtectedData object.
746 | .DESCRIPTION
747 | The KeyData copies in a ProtectedData object which are associated with the specified Certificates and/or Passwords are removed from the object, unless that removal would leave no KeyData copies behind.
748 | .PARAMETER InputObject
749 | The ProtectedData object which is to be modified.
750 | .PARAMETER Certificate
751 | RSA or ECDH certificates that you wish to remove from this ProtectedData object. You can pass an X509Certificate2 object to this parameter, or you can pass in a string which contains either a path to a certificate file on the file system, a path to the certificate in the Certificate provider, or a certificate thumbprint (in which case the certificate provider will be searched to find the certificate.)
752 | .PARAMETER Password
753 | Passwords in SecureString form which are to be removed from this ProtectedData object.
754 | .PARAMETER Passthru
755 | If this switch is used, the ProtectedData object will be written to the pipeline after processing is complete.
756 | .EXAMPLE
757 | $protectedData | Remove-ProtectedDataCredential -Certificate $thumbprints -Password $passwords
758 |
759 | Removes certificates and passwords from an existing ProtectedData object.
760 | .INPUTS
761 | [PSObject]
762 |
763 | The input object should be a copy of an object that was produced by Protect-Data.
764 | .OUTPUTS
765 | None, or
766 | [PSObject]
767 | .LINK
768 | Protect-Data
769 | .LINK
770 | Unprotect-Data
771 | .LINK
772 | Add-ProtectedDataCredential
773 | #>
774 |
775 | [CmdletBinding()]
776 | param (
777 | [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
778 | [ValidateScript({
779 | if (-not (Test-IsProtectedData -InputObject $_))
780 | {
781 | throw 'InputObject argument must be a ProtectedData object.'
782 | }
783 |
784 | return $true
785 | })]
786 | $InputObject,
787 |
788 | [ValidateNotNull()]
789 | [AllowEmptyCollection()]
790 | [object[]]
791 | $Certificate,
792 |
793 | [ValidateNotNull()]
794 | [AllowEmptyCollection()]
795 | [System.Security.SecureString[]]
796 | $Password,
797 |
798 | [switch]
799 | $Passthru
800 | )
801 |
802 | begin
803 | {
804 | $thumbprints = @(
805 | $Certificate |
806 | ConvertTo-X509Certificate2 |
807 | Select-Object -ExpandProperty Thumbprint
808 | )
809 |
810 | $thumbprints = $thumbprints | Get-Unique
811 | }
812 |
813 | process
814 | {
815 | $matchingKeyData = @(
816 | foreach ($keyData in $InputObject.KeyData)
817 | {
818 | if (Test-IsCertificateProtectedKeyData -InputObject $keyData)
819 | {
820 | if ($thumbprints -contains $keyData.Thumbprint) { $keyData }
821 | }
822 | elseif (Test-IsPasswordProtectedKeyData -InputObject $keyData)
823 | {
824 | foreach ($secureString in $Password)
825 | {
826 | $params = @{
827 | Password = $secureString
828 | Salt = $keyData.HashSalt
829 | IterationCount = $keyData.IterationCount
830 | }
831 | if ($keyData.Hash -eq (Get-PasswordHash @params))
832 | {
833 | $keyData
834 | }
835 | }
836 | }
837 | }
838 | )
839 |
840 | if ($matchingKeyData.Count -eq $InputObject.KeyData.Count)
841 | {
842 | Write-Error 'You must leave at least one copy of the ProtectedData object''s keys.'
843 | return
844 | }
845 |
846 | $InputObject.KeyData = $InputObject.KeyData | Where-Object { $matchingKeyData -notcontains $_ }
847 |
848 | if ($Passthru)
849 | {
850 | $InputObject
851 | }
852 | }
853 |
854 | } # function Remove-ProtectedDataCredential
855 |
856 | function Get-ProtectedDataSupportedTypes
857 | {
858 | <#
859 | .Synopsis
860 | Returns a list of types that can be used as the InputObject in the Protect-Data command.
861 | .EXAMPLE
862 | $types = Get-ProtectedDataSupportedTypes
863 | .INPUTS
864 | None.
865 | .OUTPUTS
866 | Type[]
867 | .NOTES
868 | This function allows you to know which InputObject types are supported by the Protect-Data and Unprotect-Data commands in this version of the module. This list may expand over time, will always be backwards-compatible with previously-encrypted data.
869 | .LINK
870 | Protect-Data
871 | .LINK
872 | Unprotect-Data
873 | #>
874 |
875 | [CmdletBinding()]
876 | [OutputType([Type[]])]
877 | param ( )
878 |
879 | $script:ValidTypes
880 | }
881 |
882 | function Get-KeyEncryptionCertificate
883 | {
884 | <#
885 | .Synopsis
886 | Finds certificates which can be used by Protect-Data and related commands.
887 | .DESCRIPTION
888 | Searches the given path, and all child paths, for certificates which can be used by Protect-Data. Such certificates must support Key Encipherment (for RSA) or Key Agreement (for ECDH) usage, and by default, must not be expired and must be issued by a trusted authority.
889 | .PARAMETER Path
890 | Path which should be searched for the certifictes. Defaults to the entire Cert: drive.
891 | .PARAMETER CertificateThumbprint
892 | Thumbprints which should be included in the search. Wildcards are allowed. Defaults to '*'.
893 | .PARAMETER SkipCertificateVerification
894 | Deprecated parameter, which will be removed in a future release. Specifying this switch will generate a warning.
895 | .PARAMETER RequirePrivateKey
896 | If this switch is used, the command will only output certificates which have a usable private key on this computer.
897 | .EXAMPLE
898 | Get-KeyEncryptionCertificate -Path Cert:\CurrentUser -RequirePrivateKey
899 |
900 | Searches for certificates which support key encipherment (RSA) or key agreement (ECDH) and have a private key installed. All matching certificates are returned.
901 | .EXAMPLE
902 | Get-KeyEncryptionCertificate -Path Cert:\CurrentUser\TrustedPeople
903 |
904 | Searches the current user's Trusted People store for certificates that can be used with Protect-Data. Certificates do not need to have a private key available to the current user.
905 | .INPUTS
906 | None.
907 | .OUTPUTS
908 | [System.Security.Cryptography.X509Certificates.X509Certificate2]
909 | .LINK
910 | Protect-Data
911 | .LINK
912 | Unprotect-Data
913 | .LINK
914 | Add-ProtectedDataCredential
915 | .LINK
916 | Remove-ProtectedDataCredential
917 | #>
918 |
919 | [CmdletBinding()]
920 | [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2])]
921 | param (
922 | [ValidateNotNullOrEmpty()]
923 | [string]
924 | $Path = 'Cert:\',
925 |
926 | [string]
927 | $CertificateThumbprint = '*',
928 |
929 | [switch]
930 | $SkipCertificateVerification,
931 |
932 | [switch]
933 | $RequirePrivateKey
934 | )
935 |
936 | if ($PSBoundParameters.ContainsKey('SkipCertificateVerification'))
937 | {
938 | Write-Warning 'The -SkipCertificateVerification switch has been deprecated, and the module now treats that as its default behavior. This switch will be removed in a future release.'
939 | }
940 |
941 | # Suppress error output if we're doing a wildcard search (unless user specifically asks for it via -ErrorAction)
942 | # This is a little ugly, may rework this later now that I've made Get-KeyEncryptionCertificate public. Originally
943 | # it was only used to search for a single thumbprint, and threw errors back to the caller if no suitable cert could
944 | # be found. Now I want it to also be used as a search tool for users to identify suitable certificates. Maybe just
945 | # needs to be two separate functions, one internal and one public.
946 |
947 | if (-not $PSBoundParameters.ContainsKey('ErrorAction') -and
948 | $CertificateThumbprint -notmatch '^[A-F\d]+$')
949 | {
950 | $ErrorActionPreference = $IgnoreError
951 | }
952 |
953 | $certGroups = GetCertificateByThumbprint -Path $Path -Thumbprint $CertificateThumbprint -ErrorAction $IgnoreError |
954 | Group-Object -Property Thumbprint
955 |
956 | if ($null -eq $certGroups)
957 | {
958 | throw "Certificate '$CertificateThumbprint' was not found."
959 | }
960 |
961 | foreach ($group in $certGroups)
962 | {
963 | ValidateKeyEncryptionCertificate -CertificateGroup $group.Group -RequirePrivateKey:$RequirePrivateKey
964 | }
965 |
966 | } # function Get-KeyEncryptionCertificate
967 |
968 | #endregion
969 |
970 | #region Helper functions
971 |
972 | function ConvertTo-X509Certificate2
973 | {
974 | [CmdletBinding()]
975 | [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2])]
976 | param (
977 | [Parameter(ValueFromPipeline = $true)]
978 | [object[]] $InputObject = @()
979 | )
980 |
981 | process
982 | {
983 | foreach ($object in $InputObject)
984 | {
985 | if ($null -eq $object) { continue }
986 |
987 | $possibleCerts = @(
988 | $object -as [System.Security.Cryptography.X509Certificates.X509Certificate2]
989 | GetCertificateFromPSPath -Path $object
990 | ) -ne $null
991 |
992 | if ($object -match '^[A-F\d]+$' -and $possibleCerts.Count -eq 0)
993 | {
994 | $possibleCerts = @(GetCertificateByThumbprint -Thumbprint $object)
995 | }
996 |
997 | $cert = $possibleCerts | Select-Object -First 1
998 |
999 | if ($null -ne $cert)
1000 | {
1001 | $cert
1002 | }
1003 | else
1004 | {
1005 | Write-Error "No certificate with identifier '$object' of type $($object.GetType().FullName) was found."
1006 | }
1007 | }
1008 | }
1009 | }
1010 |
1011 | function GetCertificateFromPSPath
1012 | {
1013 | [CmdletBinding()]
1014 | [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2])]
1015 | param (
1016 | [Parameter(Mandatory = $true)]
1017 | [string] $Path
1018 | )
1019 |
1020 | if (-not (Test-Path -LiteralPath $Path)) { return }
1021 | $resolved = Resolve-Path -LiteralPath $Path
1022 |
1023 | switch ($resolved.Provider.Name)
1024 | {
1025 | 'FileSystem'
1026 | {
1027 | # X509Certificate2 has a constructor that takes a fileName string; using the -as operator is faster than
1028 | # New-Object, and works just as well.
1029 |
1030 | return $resolved.ProviderPath -as [System.Security.Cryptography.X509Certificates.X509Certificate2]
1031 | }
1032 |
1033 | 'Certificate'
1034 | {
1035 | return (Get-Item -LiteralPath $Path) -as [System.Security.Cryptography.X509Certificates.X509Certificate2]
1036 | }
1037 | }
1038 | }
1039 |
1040 | function GetCertificateByThumbprint
1041 | {
1042 | [CmdletBinding()]
1043 | [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2])]
1044 | param (
1045 | [Parameter(Mandatory = $true)]
1046 | [string] $Thumbprint,
1047 |
1048 | [ValidateNotNullOrEmpty()]
1049 | [string]
1050 | $Path = 'Cert:\'
1051 | )
1052 |
1053 | return Get-ChildItem -Path $Path -Recurse -Include $Thumbprint |
1054 | Where-Object { $_ -is [System.Security.Cryptography.X509Certificates.X509Certificate2] } |
1055 | Sort-Object -Property HasPrivateKey -Descending
1056 | }
1057 |
1058 | function Protect-DataWithAes
1059 | {
1060 | [CmdletBinding(DefaultParameterSetName = 'KnownKey')]
1061 | param (
1062 | [Parameter(Mandatory = $true)]
1063 | [byte[]]
1064 | $PlainText,
1065 |
1066 | [byte[]]
1067 | $Key,
1068 |
1069 | [byte[]]
1070 | $InitializationVector,
1071 |
1072 | [switch]
1073 | $NoHMAC
1074 | )
1075 |
1076 | $aes = $null
1077 | $memoryStream = $null
1078 | $cryptoStream = $null
1079 |
1080 | try
1081 | {
1082 | $aes = New-Object System.Security.Cryptography.AesCryptoServiceProvider
1083 |
1084 | if ($null -ne $Key) { $aes.Key = $Key }
1085 | if ($null -ne $InitializationVector) { $aes.IV = $InitializationVector }
1086 |
1087 | $memoryStream = New-Object System.IO.MemoryStream
1088 | $cryptoStream = New-Object System.Security.Cryptography.CryptoStream(
1089 | $memoryStream, $aes.CreateEncryptor(), 'Write'
1090 | )
1091 |
1092 | $cryptoStream.Write($PlainText, 0, $PlainText.Count)
1093 | $cryptoStream.FlushFinalBlock()
1094 |
1095 | $properties = @{
1096 | CipherText = $memoryStream.ToArray()
1097 | HMAC = $null
1098 | }
1099 |
1100 | $hmacKeySplat = @{
1101 | Key = $Key
1102 | }
1103 |
1104 | if ($null -eq $Key)
1105 | {
1106 | $properties['Key'] = New-Object PowerShellUtils.PinnedArray[byte](,$aes.Key)
1107 | $hmacKeySplat['Key'] = $properties['Key']
1108 | }
1109 |
1110 | if ($null -eq $InitializationVector)
1111 | {
1112 | $properties['IV'] = New-Object PowerShellUtils.PinnedArray[byte](,$aes.IV)
1113 | }
1114 |
1115 | if (-not $NoHMAC)
1116 | {
1117 | $properties['HMAC'] = Get-Hmac @hmacKeySplat -Bytes $properties['CipherText']
1118 | }
1119 |
1120 | New-Object psobject -Property $properties
1121 | }
1122 | finally
1123 | {
1124 | if ($null -ne $aes) { $aes.Clear() }
1125 | if ($cryptoStream -is [IDisposable]) { $cryptoStream.Dispose() }
1126 | if ($memoryStream -is [IDisposable]) { $memoryStream.Dispose() }
1127 | }
1128 | }
1129 |
1130 | function Get-Hmac
1131 | {
1132 | [OutputType([byte[]])]
1133 | param (
1134 | [Parameter(Mandatory = $true)]
1135 | [byte[]] $Key,
1136 |
1137 | [Parameter(Mandatory = $true)]
1138 | [byte[]] $Bytes
1139 | )
1140 |
1141 | $hmac = $null
1142 | $sha = $null
1143 |
1144 | try
1145 | {
1146 | $sha = New-Object System.Security.Cryptography.SHA256CryptoServiceProvider
1147 | $hmac = New-Object PowerShellUtils.FipsHmacSha256(,$sha.ComputeHash($Key))
1148 | return ,$hmac.ComputeHash($Bytes)
1149 | }
1150 | finally
1151 | {
1152 | if ($null -ne $hmac) { $hmac.Clear() }
1153 | if ($null -ne $sha) { $sha.Clear() }
1154 | }
1155 | }
1156 |
1157 | function Unprotect-DataWithAes
1158 | {
1159 | [CmdletBinding()]
1160 | param (
1161 | [Parameter(Mandatory = $true)]
1162 | [byte[]]
1163 | $CipherText,
1164 |
1165 | [Parameter(Mandatory = $true)]
1166 | [byte[]]
1167 | $Key,
1168 |
1169 | [Parameter(Mandatory = $true)]
1170 | [byte[]]
1171 | $InitializationVector,
1172 |
1173 | [byte[]]
1174 | $HMAC
1175 | )
1176 |
1177 | $aes = $null
1178 | $memoryStream = $null
1179 | $cryptoStream = $null
1180 | $buffer = $null
1181 |
1182 | if ($null -ne $HMAC)
1183 | {
1184 | Assert-ValidHmac -Key $Key -Bytes $CipherText -Hmac $HMAC
1185 | }
1186 |
1187 | try
1188 | {
1189 | $aes = New-Object System.Security.Cryptography.AesCryptoServiceProvider -Property @{
1190 | Key = $Key
1191 | IV = $InitializationVector
1192 | }
1193 |
1194 | # Not sure exactly how long of a buffer we'll need to hold the decrypted data. Twice
1195 | # the ciphertext length should be more than enough.
1196 | $buffer = New-Object PowerShellUtils.PinnedArray[byte](2 * $CipherText.Count)
1197 |
1198 | $memoryStream = New-Object System.IO.MemoryStream(,$buffer)
1199 | $cryptoStream = New-Object System.Security.Cryptography.CryptoStream(
1200 | $memoryStream, $aes.CreateDecryptor(), 'Write'
1201 | )
1202 |
1203 | $cryptoStream.Write($CipherText, 0, $CipherText.Count)
1204 | $cryptoStream.FlushFinalBlock()
1205 |
1206 | $plainText = New-Object PowerShellUtils.PinnedArray[byte]($memoryStream.Position)
1207 | [Array]::Copy($buffer.Array, $plainText.Array, $memoryStream.Position)
1208 |
1209 | return New-Object psobject -Property @{
1210 | PlainText = $plainText
1211 | }
1212 | }
1213 | finally
1214 | {
1215 | if ($null -ne $aes) { $aes.Clear() }
1216 | if ($cryptoStream -is [IDisposable]) { $cryptoStream.Dispose() }
1217 | if ($memoryStream -is [IDisposable]) { $memoryStream.Dispose() }
1218 | if ($buffer -is [IDisposable]) { $buffer.Dispose() }
1219 | }
1220 | }
1221 |
1222 | function Assert-ValidHmac
1223 | {
1224 | [OutputType([void])]
1225 | param (
1226 | [Parameter(Mandatory = $true)]
1227 | [byte[]] $Key,
1228 |
1229 | [Parameter(Mandatory = $true)]
1230 | [byte[]] $Bytes,
1231 |
1232 | [Parameter(Mandatory = $true)]
1233 | [byte[]] $Hmac
1234 | )
1235 |
1236 | $recomputedHmac = Get-Hmac -Key $Key -Bytes $Bytes
1237 |
1238 | if (-not (ByteArraysAreEqual $Hmac $recomputedHmac))
1239 | {
1240 | throw 'Decryption failed due to invalid HMAC.'
1241 | }
1242 | }
1243 |
1244 | function ByteArraysAreEqual([byte[]] $First, [byte[]] $Second)
1245 | {
1246 | if ($null -eq $First) { $First = @() }
1247 | if ($null -eq $Second) { $Second = @() }
1248 |
1249 | if ($First.Length -ne $Second.Length) { return $false }
1250 |
1251 | $length = $First.Length
1252 | for ($i = 0; $i -lt $length; $i++)
1253 | {
1254 | if ($First[$i] -ne $Second[$i]) { return $false }
1255 | }
1256 |
1257 | return $true
1258 | }
1259 |
1260 | function Add-KeyData
1261 | {
1262 | [CmdletBinding()]
1263 | param (
1264 | [Parameter(Mandatory = $true)]
1265 | $InputObject,
1266 |
1267 | [Parameter(Mandatory = $true)]
1268 | [byte[]]
1269 | $Key,
1270 |
1271 | [Parameter(Mandatory = $true)]
1272 | [byte[]]
1273 | $InitializationVector,
1274 |
1275 | [ValidateNotNull()]
1276 | [AllowEmptyCollection()]
1277 | [System.Security.Cryptography.X509Certificates.X509Certificate2[]]
1278 | $Certificate = @(),
1279 |
1280 | [switch]
1281 | $UseLegacyPadding,
1282 |
1283 | [ValidateNotNull()]
1284 | [AllowEmptyCollection()]
1285 | [System.Security.SecureString[]]
1286 | $Password = @(),
1287 |
1288 | [ValidateRange(1,2147483647)]
1289 | [int]
1290 | $PasswordIterationCount = 50000
1291 | )
1292 |
1293 | if ($certs.Count -eq 0 -and $Password.Count -eq 0)
1294 | {
1295 | return
1296 | }
1297 |
1298 | $useOAEP = -not $UseLegacyPadding
1299 |
1300 | $InputObject.KeyData += @(
1301 | foreach ($cert in $Certificate)
1302 | {
1303 | $match = $InputObject.KeyData |
1304 | Where-Object { $_.Thumbprint -eq $cert.Thumbprint }
1305 |
1306 | if ($null -ne $match) { continue }
1307 | Protect-KeyDataWithCertificate -Certificate $cert -Key $Key -InitializationVector $InitializationVector -UseLegacyPadding:$UseLegacyPadding
1308 | }
1309 |
1310 | foreach ($secureString in $Password)
1311 | {
1312 | $match = $InputObject.KeyData |
1313 | Where-Object {
1314 | $params = @{
1315 | Password = $secureString
1316 | Salt = $_.HashSalt
1317 | IterationCount = $_.IterationCount
1318 | }
1319 |
1320 | $null -ne $_.Hash -and $_.Hash -eq (Get-PasswordHash @params)
1321 | }
1322 |
1323 | if ($null -ne $match) { continue }
1324 | Protect-KeyDataWithPassword -Password $secureString -Key $Key -InitializationVector $InitializationVector -IterationCount $PasswordIterationCount
1325 | }
1326 | )
1327 |
1328 | } # function Add-KeyData
1329 |
1330 | function Unprotect-MatchingKeyData
1331 | {
1332 | [CmdletBinding()]
1333 | param (
1334 | [Parameter(Mandatory = $true)]
1335 | $InputObject,
1336 |
1337 | [Parameter(Mandatory = $true, ParameterSetName = 'Certificate')]
1338 | [System.Security.Cryptography.X509Certificates.X509Certificate2]
1339 | $Certificate,
1340 |
1341 | [Parameter(Mandatory = $true, ParameterSetName = 'Password')]
1342 | [System.Security.SecureString]
1343 | $Password
1344 | )
1345 |
1346 | if ($PSCmdlet.ParameterSetName -eq 'Certificate')
1347 | {
1348 | $keyData = $InputObject.KeyData |
1349 | Where-Object { (Test-IsCertificateProtectedKeyData -InputObject $_) -and $_.Thumbprint -eq $Certificate.Thumbprint } |
1350 | Select-Object -First 1
1351 |
1352 | if ($null -eq $keyData)
1353 | {
1354 | throw "Protected data object was not encrypted with certificate '$($Certificate.Thumbprint)'."
1355 | }
1356 |
1357 | try
1358 | {
1359 | return Unprotect-KeyDataWithCertificate -KeyData $keyData -Certificate $Certificate
1360 | }
1361 | catch
1362 | {
1363 | throw
1364 | }
1365 | }
1366 | else
1367 | {
1368 | $keyData =
1369 | $InputObject.KeyData |
1370 | Where-Object {
1371 | (Test-IsPasswordProtectedKeyData -InputObject $_) -and
1372 | $_.Hash -eq (Get-PasswordHash -Password $Password -Salt $_.HashSalt -IterationCount $_.IterationCount)
1373 | } |
1374 | Select-Object -First 1
1375 |
1376 | if ($null -eq $keyData)
1377 | {
1378 | throw 'Protected data object was not encrypted with the specified password.'
1379 | }
1380 |
1381 | try
1382 | {
1383 | return Unprotect-KeyDataWithPassword -KeyData $keyData -Password $Password
1384 | }
1385 | catch
1386 | {
1387 | throw
1388 | }
1389 | }
1390 |
1391 | } # function Unprotect-MatchingKeyData
1392 |
1393 | function ValidateKeyEncryptionCertificate
1394 | {
1395 | [CmdletBinding()]
1396 | [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2])]
1397 | param (
1398 | [System.Security.Cryptography.X509Certificates.X509Certificate2[]]
1399 | $CertificateGroup,
1400 |
1401 | [switch]
1402 | $RequirePrivateKey
1403 | )
1404 |
1405 | process
1406 | {
1407 | $Certificate = $CertificateGroup[0]
1408 |
1409 | $isEccCertificate = $Certificate.GetKeyAlgorithm() -eq $script:EccAlgorithmOid
1410 |
1411 | if ($Certificate.PublicKey.Key -isnot [System.Security.Cryptography.RSACryptoServiceProvider] -and
1412 | -not $isEccCertificate)
1413 | {
1414 | Write-Error "Certficiate '$($Certificate.Thumbprint)' is not an RSA or ECDH certificate."
1415 | return
1416 | }
1417 |
1418 | if ($isEccCertificate)
1419 | {
1420 | $neededKeyUsage = [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::KeyAgreement
1421 | }
1422 | else
1423 | {
1424 | $neededKeyUsage = [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::KeyEncipherment
1425 | }
1426 |
1427 | $keyUsageFlags = 0
1428 |
1429 | foreach ($extension in $Certificate.Extensions)
1430 | {
1431 | if ($extension -is [System.Security.Cryptography.X509Certificates.X509KeyUsageExtension])
1432 | {
1433 | $keyUsageFlags = $keyUsageFlags -bor $extension.KeyUsages
1434 | }
1435 | }
1436 |
1437 | if (($keyUsageFlags -band $neededKeyUsage) -ne $neededKeyUsage)
1438 | {
1439 | Write-Error "Certificate '$($Certificate.Thumbprint)' does not have the required $($neededKeyUsage.ToString()) Key Usage flag."
1440 | return
1441 | }
1442 |
1443 | if ($RequirePrivateKey)
1444 | {
1445 | $Certificate = $CertificateGroup |
1446 | Where-Object { TestPrivateKey -Certificate $_ } |
1447 | Select-Object -First 1
1448 |
1449 | if ($null -eq $Certificate)
1450 | {
1451 | Write-Error "Could not find private key for certificate '$($CertificateGroup[0].Thumbprint)'."
1452 | return
1453 | }
1454 | }
1455 |
1456 | $Certificate
1457 |
1458 | } # process
1459 |
1460 | } # function ValidateKeyEncryptionCertificate
1461 |
1462 | function TestPrivateKey([System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate)
1463 | {
1464 | if (-not $Certificate.HasPrivateKey) { return $false }
1465 | if ($Certificate.PrivateKey -is [System.Security.Cryptography.RSACryptoServiceProvider]) { return $true }
1466 |
1467 | $cngKey = $null
1468 | try
1469 | {
1470 | if ([Security.Cryptography.X509Certificates.X509CertificateExtensionMethods]::HasCngKey($Certificate))
1471 | {
1472 | $cngKey = [Security.Cryptography.X509Certificates.X509Certificate2ExtensionMethods]::GetCngPrivateKey($Certificate)
1473 | return $null -ne $cngKey -and
1474 | ($cngKey.AlgorithmGroup -eq [System.Security.Cryptography.CngAlgorithmGroup]::Rsa -or
1475 | $cngKey.AlgorithmGroup -eq [System.Security.Cryptography.CngAlgorithmGroup]::ECDiffieHellman)
1476 | }
1477 | }
1478 | catch
1479 | {
1480 | return $false
1481 | }
1482 | finally
1483 | {
1484 | if ($cngKey -is [IDisposable]) { $cngKey.Dispose() }
1485 | }
1486 | }
1487 |
1488 | function Get-KeyGenerator
1489 | {
1490 | [CmdletBinding(DefaultParameterSetName = 'CreateNew')]
1491 | [OutputType([System.Security.Cryptography.Rfc2898DeriveBytes])]
1492 | param (
1493 | [Parameter(Mandatory = $true)]
1494 | [System.Security.SecureString]
1495 | $Password,
1496 |
1497 | [Parameter(Mandatory = $true, ParameterSetName = 'RestoreExisting')]
1498 | [byte[]]
1499 | $Salt,
1500 |
1501 | [ValidateRange(1,2147483647)]
1502 | [int]
1503 | $IterationCount = 50000
1504 | )
1505 |
1506 | $byteArray = $null
1507 |
1508 | try
1509 | {
1510 | $byteArray = Convert-SecureStringToPinnedByteArray -SecureString $Password
1511 |
1512 | if ($PSCmdlet.ParameterSetName -eq 'RestoreExisting')
1513 | {
1514 | $saltBytes = $Salt
1515 | }
1516 | else
1517 | {
1518 | $saltBytes = Get-RandomBytes -Count 32
1519 | }
1520 |
1521 | New-Object System.Security.Cryptography.Rfc2898DeriveBytes($byteArray, $saltBytes, $IterationCount)
1522 | }
1523 | finally
1524 | {
1525 | if ($byteArray -is [IDisposable]) { $byteArray.Dispose() }
1526 | }
1527 |
1528 | } # function Get-KeyGenerator
1529 |
1530 | function Get-PasswordHash
1531 | {
1532 | [CmdletBinding()]
1533 | [OutputType([string])]
1534 | param (
1535 | [Parameter(Mandatory = $true)]
1536 | [System.Security.SecureString]
1537 | $Password,
1538 |
1539 | [Parameter(Mandatory = $true)]
1540 | [byte[]]
1541 | $Salt,
1542 |
1543 | [ValidateRange(1, 2147483647)]
1544 | [int]
1545 | $IterationCount = 50000
1546 | )
1547 |
1548 | $keyGen = $null
1549 |
1550 | try
1551 | {
1552 | $keyGen = Get-KeyGenerator @PSBoundParameters
1553 | [BitConverter]::ToString($keyGen.GetBytes(32)) -replace '[^A-F\d]'
1554 | }
1555 | finally
1556 | {
1557 | if ($keyGen -is [IDisposable]) { $keyGen.Dispose() }
1558 | }
1559 |
1560 | } # function Get-PasswordHash
1561 |
1562 | function Get-RandomBytes
1563 | {
1564 | [CmdletBinding()]
1565 | [OutputType([byte[]])]
1566 | param (
1567 | [Parameter(Mandatory = $true)]
1568 | [ValidateRange(1,1000)]
1569 | $Count
1570 | )
1571 |
1572 | $rng = $null
1573 |
1574 | try
1575 | {
1576 | $rng = New-Object System.Security.Cryptography.RNGCryptoServiceProvider
1577 | $bytes = New-Object byte[]($Count)
1578 | $rng.GetBytes($bytes)
1579 |
1580 | ,$bytes
1581 | }
1582 | finally
1583 | {
1584 | if ($rng -is [IDisposable]) { $rng.Dispose() }
1585 | }
1586 |
1587 | } # function Get-RandomBytes
1588 |
1589 | function Protect-KeyDataWithCertificate
1590 | {
1591 | [CmdletBinding()]
1592 | param (
1593 | [Parameter(Mandatory = $true)]
1594 | [System.Security.Cryptography.X509Certificates.X509Certificate2]
1595 | $Certificate,
1596 |
1597 | [byte[]]
1598 | $Key,
1599 |
1600 | [byte[]]
1601 | $InitializationVector,
1602 |
1603 | [switch] $UseLegacyPadding
1604 | )
1605 |
1606 | if ($Certificate.PublicKey.Key -is [System.Security.Cryptography.RSACryptoServiceProvider])
1607 | {
1608 | Protect-KeyDataWithRsaCertificate -Certificate $Certificate -Key $Key -InitializationVector $InitializationVector -UseLegacyPadding:$UseLegacyPadding
1609 | }
1610 | elseif ($Certificate.GetKeyAlgorithm() -eq $script:EccAlgorithmOid)
1611 | {
1612 | Protect-KeyDataWithEcdhCertificate -Certificate $Certificate -Key $Key -InitializationVector $InitializationVector
1613 | }
1614 | }
1615 |
1616 | function Protect-KeyDataWithRsaCertificate
1617 | {
1618 | [CmdletBinding()]
1619 | param (
1620 | [Parameter(Mandatory = $true)]
1621 | [System.Security.Cryptography.X509Certificates.X509Certificate2]
1622 | $Certificate,
1623 |
1624 | [byte[]]
1625 | $Key,
1626 |
1627 | [byte[]]
1628 | $InitializationVector,
1629 |
1630 | [switch] $UseLegacyPadding
1631 | )
1632 |
1633 | $useOAEP = -not $UseLegacyPadding
1634 |
1635 | try
1636 | {
1637 | New-Object psobject -Property @{
1638 | Key = $Certificate.PublicKey.Key.Encrypt($key, $useOAEP)
1639 | IV = $Certificate.PublicKey.Key.Encrypt($InitializationVector, $useOAEP)
1640 | Thumbprint = $Certificate.Thumbprint
1641 | LegacyPadding = [bool] $UseLegacyPadding
1642 | }
1643 | }
1644 | catch
1645 | {
1646 | Write-Error -ErrorRecord $_
1647 | }
1648 | }
1649 |
1650 | function Protect-KeyDataWithEcdhCertificate
1651 | {
1652 | [CmdletBinding()]
1653 | param (
1654 | [Parameter(Mandatory = $true)]
1655 | [System.Security.Cryptography.X509Certificates.X509Certificate2]
1656 | $Certificate,
1657 |
1658 | [byte[]]
1659 | $Key,
1660 |
1661 | [byte[]]
1662 | $InitializationVector
1663 | )
1664 |
1665 | $publicKey = $null
1666 | $ephemeralKey = $null
1667 | $ecdh = $null
1668 | $derivedKey = $null
1669 |
1670 | try
1671 | {
1672 | $publicKey = Get-EcdhPublicKey -Certificate $cert
1673 |
1674 | $ephemeralKey = [System.Security.Cryptography.CngKey]::Create($publicKey.Algorithm)
1675 | $ecdh = [System.Security.Cryptography.ECDiffieHellmanCng]$ephemeralKey
1676 |
1677 | $derivedKey = New-Object PowerShellUtils.PinnedArray[byte](
1678 | ,($ecdh.DeriveKeyMaterial($publicKey) | Select-Object -First 32)
1679 | )
1680 |
1681 | if ($derivedKey.Count -ne 32)
1682 | {
1683 | # This shouldn't happen, but just in case...
1684 | throw "Error: Key material derived from ECDH certificate $($Certificate.Thumbprint) was less than the required 32 bytes"
1685 | }
1686 |
1687 | $ecdhIv = Get-RandomBytes -Count 16
1688 |
1689 | $encryptedKey = Protect-DataWithAes -PlainText $Key -Key $derivedKey -InitializationVector $ecdhIv -NoHMAC
1690 | $encryptedIv = Protect-DataWithAes -PlainText $InitializationVector -Key $derivedKey -InitializationVector $ecdhIv -NoHMAC
1691 |
1692 | New-Object psobject -Property @{
1693 | Key = $encryptedKey.CipherText
1694 | IV = $encryptedIv.CipherText
1695 | EcdhPublicKey = $ecdh.PublicKey.ToByteArray()
1696 | EcdhIV = $ecdhIv
1697 | Thumbprint = $Certificate.Thumbprint
1698 | }
1699 | }
1700 | finally
1701 | {
1702 | if ($publicKey -is [IDisposable]) { $publicKey.Dispose() }
1703 | if ($ephemeralKey -is [IDisposable]) { $ephemeralKey.Dispose() }
1704 | if ($null -ne $ecdh) { $ecdh.Clear() }
1705 | if ($derivedKey -is [IDisposable]) { $derivedKey.Dispose() }
1706 | }
1707 | }
1708 |
1709 | function Get-EcdhPublicKey([System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate)
1710 | {
1711 | # If we get here, we've already verified that the certificate has the Key Agreement usage extension,
1712 | # and that it is an ECC algorithm cert, meaning we can treat the OIDs as ECDH algorithms. (These OIDs
1713 | # are shared with ECDSA, for some reason, and the ECDSA magic constants are different.)
1714 |
1715 | $magic = @{
1716 | '1.2.840.10045.3.1.7' = [uint32]0x314B4345L # BCRYPT_ECDH_PUBLIC_P256_MAGIC
1717 | '1.3.132.0.34' = [uint32]0x334B4345L # BCRYPT_ECDH_PUBLIC_P384_MAGIC
1718 | '1.3.132.0.35' = [uint32]0x354B4345L # BCRYPT_ECDH_PUBLIC_P521_MAGIC
1719 | }
1720 |
1721 | $algorithm = Get-AlgorithmOid -Certificate $Certificate
1722 |
1723 | if (-not $magic.ContainsKey($algorithm))
1724 | {
1725 | throw "Certificate '$($Certificate.Thumbprint)' returned an unknown Public Key Algorithm OID: '$algorithm'"
1726 | }
1727 |
1728 | $size = (($cert.GetPublicKey().Count - 1) / 2)
1729 |
1730 | $keyBlob = [byte[]]@(
1731 | [System.BitConverter]::GetBytes($magic[$algorithm])
1732 | [System.BitConverter]::GetBytes($size)
1733 | $cert.GetPublicKey() | Select-Object -Skip 1
1734 | )
1735 |
1736 | return [System.Security.Cryptography.CngKey]::Import($keyBlob, [System.Security.Cryptography.CngKeyBlobFormat]::EccPublicBlob)
1737 | }
1738 |
1739 |
1740 | function Get-AlgorithmOid([System.Security.Cryptography.X509Certificates.X509Certificate] $Certificate)
1741 | {
1742 | $algorithmOid = $Certificate.GetKeyAlgorithm();
1743 |
1744 | if ($algorithmOid -eq $script:EccAlgorithmOid)
1745 | {
1746 | $algorithmOid = DecodeBinaryOid -Bytes $Certificate.GetKeyAlgorithmParameters()
1747 | }
1748 |
1749 | return $algorithmOid
1750 | }
1751 |
1752 | function DecodeBinaryOid([byte[]] $Bytes)
1753 | {
1754 | # Thanks to Vadims Podans (http://sysadmins.lv/) for this cool technique to take a byte array
1755 | # and decode the OID without having to use P/Invoke to call the CryptDecodeObject function directly.
1756 |
1757 | [byte[]] $ekuBlob = @(
1758 | 48
1759 | $Bytes.Count
1760 | $Bytes
1761 | )
1762 |
1763 | $asnEncodedData = New-Object System.Security.Cryptography.AsnEncodedData(,$ekuBlob)
1764 | $enhancedKeyUsage = New-Object System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension($asnEncodedData, $false)
1765 |
1766 | return $enhancedKeyUsage.EnhancedKeyUsages[0].Value
1767 | }
1768 |
1769 | function Unprotect-KeyDataWithCertificate
1770 | {
1771 | [CmdletBinding()]
1772 | param (
1773 | [Parameter(Mandatory = $true)]
1774 | $KeyData,
1775 |
1776 | [Parameter(Mandatory = $true)]
1777 | [System.Security.Cryptography.X509Certificates.X509Certificate2]
1778 | $Certificate
1779 | )
1780 |
1781 | if ($Certificate.PublicKey.Key -is [System.Security.Cryptography.RSACryptoServiceProvider])
1782 | {
1783 | Unprotect-KeyDataWithRsaCertificate -KeyData $KeyData -Certificate $Certificate
1784 | }
1785 | elseif ($Certificate.GetKeyAlgorithm() -eq $script:EccAlgorithmOid)
1786 | {
1787 | Unprotect-KeyDataWithEcdhCertificate -KeyData $KeyData -Certificate $Certificate
1788 | }
1789 | }
1790 |
1791 | function Unprotect-KeyDataWithEcdhCertificate
1792 | {
1793 | [CmdletBinding()]
1794 | param (
1795 | [Parameter(Mandatory = $true)]
1796 | $KeyData,
1797 |
1798 | [Parameter(Mandatory = $true)]
1799 | [System.Security.Cryptography.X509Certificates.X509Certificate2]
1800 | $Certificate
1801 | )
1802 |
1803 | $doFinallyBlock = $true
1804 | $key = $null
1805 | $iv = $null
1806 | $derivedKey = $null
1807 | $publicKey = $null
1808 | $privateKey = $null
1809 | $ecdh = $null
1810 |
1811 | try
1812 | {
1813 | $privateKey = [Security.Cryptography.X509Certificates.X509Certificate2ExtensionMethods]::GetCngPrivateKey($Certificate)
1814 |
1815 | if ($privateKey.AlgorithmGroup -ne [System.Security.Cryptography.CngAlgorithmGroup]::ECDiffieHellman)
1816 | {
1817 | throw "Certificate '$($Certificate.Thumbprint)' contains a non-ECDH key pair."
1818 | }
1819 |
1820 | if ($null -eq $KeyData.EcdhPublicKey -or $null -eq $KeyData.EcdhIV)
1821 | {
1822 | throw "Certificate '$($Certificate.Thumbprint)' is a valid ECDH certificate, but the stored KeyData structure is missing the public key and/or IV used during encryption."
1823 | }
1824 |
1825 | $publicKey = [System.Security.Cryptography.CngKey]::Import($KeyData.EcdhPublicKey, [System.Security.Cryptography.CngKeyBlobFormat]::EccPublicBlob)
1826 | $ecdh = [System.Security.Cryptography.ECDiffieHellmanCng]$privateKey
1827 |
1828 | $derivedKey = New-Object PowerShellUtils.PinnedArray[byte](,($ecdh.DeriveKeyMaterial($publicKey) | Select-Object -First 32))
1829 | if ($derivedKey.Count -ne 32)
1830 | {
1831 | # This shouldn't happen, but just in case...
1832 | throw "Error: Key material derived from ECDH certificate $($Certificate.Thumbprint) was less than the required 32 bytes"
1833 | }
1834 |
1835 | $key = (Unprotect-DataWithAes -CipherText $KeyData.Key -Key $derivedKey -InitializationVector $KeyData.EcdhIV).PlainText
1836 | $iv = (Unprotect-DataWithAes -CipherText $KeyData.IV -Key $derivedKey -InitializationVector $KeyData.EcdhIV).PlainText
1837 |
1838 | $doFinallyBlock = $false
1839 |
1840 | return New-Object psobject -Property @{
1841 | Key = $key
1842 | IV = $iv
1843 | }
1844 | }
1845 | catch
1846 | {
1847 | throw
1848 | }
1849 | finally
1850 | {
1851 | if ($doFinallyBlock)
1852 | {
1853 | if ($key -is [IDisposable]) { $key.Dispose() }
1854 | if ($iv -is [IDisposable]) { $iv.Dispose() }
1855 | }
1856 |
1857 | if ($derivedKey -is [IDisposable]) { $derivedKey.Dispose() }
1858 | if ($privateKey -is [IDisposable]) { $privateKey.Dispose() }
1859 | if ($publicKey -is [IDisposable]) { $publicKey.Dispose() }
1860 | if ($null -ne $ecdh) { $ecdh.Clear() }
1861 | }
1862 | }
1863 |
1864 | function Unprotect-KeyDataWithRsaCertificate
1865 | {
1866 | [CmdletBinding()]
1867 | param (
1868 | [Parameter(Mandatory = $true)]
1869 | $KeyData,
1870 |
1871 | [Parameter(Mandatory = $true)]
1872 | [System.Security.Cryptography.X509Certificates.X509Certificate2]
1873 | $Certificate
1874 | )
1875 |
1876 | $useOAEP = -not $keyData.LegacyPadding
1877 |
1878 | $key = $null
1879 | $iv = $null
1880 | $doFinallyBlock = $true
1881 |
1882 | try
1883 | {
1884 | $key = DecryptRsaData -Certificate $Certificate -CipherText $keyData.Key -UseOaepPadding:$useOAEP
1885 | $iv = DecryptRsaData -Certificate $Certificate -CipherText $keyData.IV -UseOaepPadding:$useOAEP
1886 |
1887 | $doFinallyBlock = $false
1888 |
1889 | return New-Object psobject -Property @{
1890 | Key = $key
1891 | IV = $iv
1892 | }
1893 | }
1894 | catch
1895 | {
1896 | throw
1897 | }
1898 | finally
1899 | {
1900 | if ($doFinallyBlock)
1901 | {
1902 | if ($key -is [IDisposable]) { $key.Dispose() }
1903 | if ($iv -is [IDisposable]) { $iv.Dispose() }
1904 | }
1905 | }
1906 | }
1907 |
1908 | function DecryptRsaData([System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate,
1909 | [byte[]] $CipherText,
1910 | [switch] $UseOaepPadding)
1911 | {
1912 | if ($Certificate.PrivateKey -is [System.Security.Cryptography.RSACryptoServiceProvider])
1913 | {
1914 | return New-Object PowerShellUtils.PinnedArray[byte](
1915 | ,$Certificate.PrivateKey.Decrypt($CipherText, $UseOaepPadding)
1916 | )
1917 | }
1918 |
1919 | # By the time we get here, we've already validated that either the certificate has an RsaCryptoServiceProvider
1920 | # object in its PrivateKey property, or we can fetch an RSA CNG key.
1921 |
1922 | $cngKey = $null
1923 | $cngRsa = $null
1924 | try
1925 | {
1926 | $cngKey = [Security.Cryptography.X509Certificates.X509Certificate2ExtensionMethods]::GetCngPrivateKey($Certificate)
1927 | $cngRsa = [Security.Cryptography.RSACng]$cngKey
1928 | $cngRsa.EncryptionHashAlgorithm = [System.Security.Cryptography.CngAlgorithm]::Sha1
1929 |
1930 | if (-not $UseOaepPadding)
1931 | {
1932 | $cngRsa.EncryptionPaddingMode = [Security.Cryptography.AsymmetricPaddingMode]::Pkcs1
1933 | }
1934 |
1935 | return New-Object PowerShellUtils.PinnedArray[byte](
1936 | ,$cngRsa.DecryptValue($CipherText)
1937 | )
1938 | }
1939 | catch
1940 | {
1941 | throw
1942 | }
1943 | finally
1944 | {
1945 | if ($cngKey -is [IDisposable]) { $cngKey.Dispose() }
1946 | if ($null -ne $cngRsa) { $cngRsa.Clear() }
1947 | }
1948 | }
1949 |
1950 | function Protect-KeyDataWithPassword
1951 | {
1952 | [CmdletBinding()]
1953 | param (
1954 | [Parameter(Mandatory = $true)]
1955 | [System.Security.SecureString]
1956 | $Password,
1957 |
1958 | [Parameter(Mandatory = $true)]
1959 | [byte[]]
1960 | $Key,
1961 |
1962 | [Parameter(Mandatory = $true)]
1963 | [byte[]]
1964 | $InitializationVector,
1965 |
1966 | [ValidateRange(1,2147483647)]
1967 | [int]
1968 | $IterationCount = 50000
1969 | )
1970 |
1971 | $keyGen = $null
1972 | $ephemeralKey = $null
1973 | $ephemeralIV = $null
1974 |
1975 | try
1976 | {
1977 | $keyGen = Get-KeyGenerator -Password $Password -IterationCount $IterationCount
1978 | $ephemeralKey = New-Object PowerShellUtils.PinnedArray[byte](,$keyGen.GetBytes(32))
1979 | $ephemeralIV = New-Object PowerShellUtils.PinnedArray[byte](,$keyGen.GetBytes(16))
1980 |
1981 | $hashSalt = Get-RandomBytes -Count 32
1982 | $hash = Get-PasswordHash -Password $Password -Salt $hashSalt -IterationCount $IterationCount
1983 |
1984 | $encryptedKey = (Protect-DataWithAes -PlainText $Key -Key $ephemeralKey -InitializationVector $ephemeralIV -NoHMAC).CipherText
1985 | $encryptedIV = (Protect-DataWithAes -PlainText $InitializationVector -Key $ephemeralKey -InitializationVector $ephemeralIV -NoHMAC).CipherText
1986 |
1987 | New-Object psobject -Property @{
1988 | Key = $encryptedKey
1989 | IV = $encryptedIV
1990 | Salt = $keyGen.Salt
1991 | IterationCount = $keyGen.IterationCount
1992 | Hash = $hash
1993 | HashSalt = $hashSalt
1994 | }
1995 | }
1996 | catch
1997 | {
1998 | throw
1999 | }
2000 | finally
2001 | {
2002 | if ($keyGen -is [IDisposable]) { $keyGen.Dispose() }
2003 | if ($ephemeralKey -is [IDisposable]) { $ephemeralKey.Dispose() }
2004 | if ($ephemeralIV -is [IDisposable]) { $ephemeralIV.Dispose() }
2005 | }
2006 |
2007 | } # function Protect-KeyDataWithPassword
2008 |
2009 | function Unprotect-KeyDataWithPassword
2010 | {
2011 | [CmdletBinding()]
2012 | param (
2013 | [Parameter(Mandatory = $true)]
2014 | $KeyData,
2015 |
2016 | [Parameter(Mandatory = $true)]
2017 | [System.Security.SecureString]
2018 | $Password
2019 | )
2020 |
2021 | $keyGen = $null
2022 | $key = $null
2023 | $iv = $null
2024 | $ephemeralKey = $null
2025 | $ephemeralIV = $null
2026 |
2027 | $doFinallyBlock = $true
2028 |
2029 | try
2030 | {
2031 | $params = @{
2032 | Password = $Password
2033 | Salt = $KeyData.Salt.Clone()
2034 | IterationCount = $KeyData.IterationCount
2035 | }
2036 |
2037 | $keyGen = Get-KeyGenerator @params
2038 | $ephemeralKey = New-Object PowerShellUtils.PinnedArray[byte](,$keyGen.GetBytes(32))
2039 | $ephemeralIV = New-Object PowerShellUtils.PinnedArray[byte](,$keyGen.GetBytes(16))
2040 |
2041 | $key = (Unprotect-DataWithAes -CipherText $KeyData.Key -Key $ephemeralKey -InitializationVector $ephemeralIV).PlainText
2042 | $iv = (Unprotect-DataWithAes -CipherText $KeyData.IV -Key $ephemeralKey -InitializationVector $ephemeralIV).PlainText
2043 |
2044 | $doFinallyBlock = $false
2045 |
2046 | return New-Object psobject -Property @{
2047 | Key = $key
2048 | IV = $iv
2049 | }
2050 | }
2051 | catch
2052 | {
2053 | throw
2054 | }
2055 | finally
2056 | {
2057 | if ($keyGen -is [IDisposable]) { $keyGen.Dispose() }
2058 | if ($ephemeralKey -is [IDisposable]) { $ephemeralKey.Dispose() }
2059 | if ($ephemeralIV -is [IDisposable]) { $ephemeralIV.Dispose() }
2060 |
2061 | if ($doFinallyBlock)
2062 | {
2063 | if ($key -is [IDisposable]) { $key.Dispose() }
2064 | if ($iv -is [IDisposable]) { $iv.Dispose() }
2065 | }
2066 | }
2067 | } # function Unprotect-KeyDataWithPassword
2068 |
2069 | function ConvertTo-PinnedByteArray
2070 | {
2071 | [CmdletBinding()]
2072 | [OutputType([PowerShellUtils.PinnedArray[byte]])]
2073 | param (
2074 | [Parameter(Mandatory = $true)]
2075 | $InputObject
2076 | )
2077 |
2078 | try
2079 | {
2080 | switch ($InputObject.GetType().FullName)
2081 | {
2082 | ([string].FullName)
2083 | {
2084 | $pinnedArray = Convert-StringToPinnedByteArray -String $InputObject
2085 | break
2086 | }
2087 |
2088 | ([System.Security.SecureString].FullName)
2089 | {
2090 | $pinnedArray = Convert-SecureStringToPinnedByteArray -SecureString $InputObject
2091 | break
2092 | }
2093 |
2094 | ([System.Management.Automation.PSCredential].FullName)
2095 | {
2096 | $pinnedArray = Convert-PSCredentialToPinnedByteArray -Credential $InputObject
2097 | break
2098 | }
2099 |
2100 | default
2101 | {
2102 | $byteArray = $InputObject -as [byte[]]
2103 |
2104 | if ($null -eq $byteArray)
2105 | {
2106 | throw 'Something unexpected got through our parameter validation.'
2107 | }
2108 | else
2109 | {
2110 | $pinnedArray = New-Object PowerShellUtils.PinnedArray[byte](
2111 | ,$byteArray.Clone()
2112 | )
2113 | }
2114 | }
2115 |
2116 | }
2117 |
2118 | $pinnedArray
2119 | }
2120 | catch
2121 | {
2122 | throw
2123 | }
2124 |
2125 | } # function ConvertTo-PinnedByteArray
2126 |
2127 | function ConvertFrom-ByteArray
2128 | {
2129 | [CmdletBinding()]
2130 | param (
2131 | [Parameter(Mandatory = $true)]
2132 | [byte[]]
2133 | $ByteArray,
2134 |
2135 | [Parameter(Mandatory = $true)]
2136 | [ValidateScript({
2137 | if ($script:ValidTypes -notcontains $_)
2138 | {
2139 | throw "Invalid type specified. Type must be one of: $($script:ValidTypes -join ', ')"
2140 | }
2141 |
2142 | return $true
2143 | })]
2144 | [type]
2145 | $Type,
2146 |
2147 | [UInt32]
2148 | $StartIndex = 0,
2149 |
2150 | [Nullable[UInt32]]
2151 | $ByteCount = $null
2152 | )
2153 |
2154 | if ($null -eq $ByteCount)
2155 | {
2156 | $ByteCount = $ByteArray.Count - $StartIndex
2157 | }
2158 |
2159 | if ($StartIndex + $ByteCount -gt $ByteArray.Count)
2160 | {
2161 | throw 'The specified index and count values exceed the bounds of the array.'
2162 | }
2163 |
2164 | switch ($Type.FullName)
2165 | {
2166 | ([string].FullName)
2167 | {
2168 | Convert-ByteArrayToString -ByteArray $ByteArray -StartIndex $StartIndex -ByteCount $ByteCount
2169 | break
2170 | }
2171 |
2172 | ([System.Security.SecureString].FullName)
2173 | {
2174 | Convert-ByteArrayToSecureString -ByteArray $ByteArray -StartIndex $StartIndex -ByteCount $ByteCount
2175 | break
2176 | }
2177 |
2178 | ([System.Management.Automation.PSCredential].FullName)
2179 | {
2180 | Convert-ByteArrayToPSCredential -ByteArray $ByteArray -StartIndex $StartIndex -ByteCount $ByteCount
2181 | break
2182 | }
2183 |
2184 | ([byte[]].FullName)
2185 | {
2186 | $array = New-Object byte[]($ByteCount)
2187 | [Array]::Copy($ByteArray, $StartIndex, $array, 0, $ByteCount)
2188 |
2189 | ,$array
2190 | break
2191 | }
2192 |
2193 | default
2194 | {
2195 | throw 'Something unexpected got through parameter validation.'
2196 | }
2197 | }
2198 |
2199 | } # function ConvertFrom-ByteArray
2200 |
2201 | function Convert-StringToPinnedByteArray
2202 | {
2203 | [CmdletBinding()]
2204 | [OutputType([PowerShellUtils.PinnedArray[byte]])]
2205 | param (
2206 | [Parameter(Mandatory = $true)]
2207 | [string]
2208 | $String
2209 | )
2210 |
2211 | New-Object PowerShellUtils.PinnedArray[byte](
2212 | ,[System.Text.Encoding]::UTF8.GetBytes($String)
2213 | )
2214 | }
2215 |
2216 | function Convert-SecureStringToPinnedByteArray
2217 | {
2218 | [CmdletBinding()]
2219 | [OutputType([PowerShellUtils.PinnedArray[byte]])]
2220 | param (
2221 | [Parameter(Mandatory = $true)]
2222 | [System.Security.SecureString]
2223 | $SecureString
2224 | )
2225 |
2226 | try
2227 | {
2228 | $ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToGlobalAllocUnicode($SecureString)
2229 | $byteCount = $SecureString.Length * 2
2230 | $pinnedArray = New-Object PowerShellUtils.PinnedArray[byte]($byteCount)
2231 |
2232 | [System.Runtime.InteropServices.Marshal]::Copy($ptr, $pinnedArray, 0, $byteCount)
2233 |
2234 | $pinnedArray
2235 | }
2236 | catch
2237 | {
2238 | throw
2239 | }
2240 | finally
2241 | {
2242 | if ($null -ne $ptr)
2243 | {
2244 | [System.Runtime.InteropServices.Marshal]::ZeroFreeGlobalAllocUnicode($ptr)
2245 | }
2246 | }
2247 |
2248 | } # function Convert-SecureStringToPinnedByteArray
2249 |
2250 | function Convert-PSCredentialToPinnedByteArray
2251 | {
2252 | [CmdletBinding()]
2253 | [OutputType([PowerShellUtils.PinnedArray[byte]])]
2254 | param (
2255 | [Parameter(Mandatory = $true)]
2256 | [System.Management.Automation.PSCredential]
2257 | $Credential
2258 | )
2259 |
2260 | $passwordBytes = $null
2261 | $pinnedArray = $null
2262 |
2263 | try
2264 | {
2265 | $passwordBytes = Convert-SecureStringToPinnedByteArray -SecureString $Credential.Password
2266 | $usernameBytes = [System.Text.Encoding]::Unicode.GetBytes($Credential.UserName)
2267 | $sizeBytes = [System.BitConverter]::GetBytes([uint32]$usernameBytes.Count)
2268 |
2269 | if (-not [System.BitConverter]::IsLittleEndian) { [Array]::Reverse($sizeBytes) }
2270 |
2271 | $doFinallyBlock = $true
2272 |
2273 | try
2274 | {
2275 | $bufferSize = $passwordBytes.Count +
2276 | $usernameBytes.Count +
2277 | $script:PSCredentialHeader.Count +
2278 | $sizeBytes.Count
2279 | $pinnedArray = New-Object PowerShellUtils.PinnedArray[byte]($bufferSize)
2280 |
2281 | $destIndex = 0
2282 |
2283 | [Array]::Copy(
2284 | $script:PSCredentialHeader, 0, $pinnedArray.Array, $destIndex, $script:PSCredentialHeader.Count
2285 | )
2286 | $destIndex += $script:PSCredentialHeader.Count
2287 |
2288 | [Array]::Copy($sizeBytes, 0, $pinnedArray.Array, $destIndex, $sizeBytes.Count)
2289 | $destIndex += $sizeBytes.Count
2290 |
2291 | [Array]::Copy($usernameBytes, 0, $pinnedArray.Array, $destIndex, $usernameBytes.Count)
2292 | $destIndex += $usernameBytes.Count
2293 |
2294 | [Array]::Copy($passwordBytes.Array, 0, $pinnedArray.Array, $destIndex, $passwordBytes.Count)
2295 |
2296 | $doFinallyBlock = $false
2297 | $pinnedArray
2298 | }
2299 | finally
2300 | {
2301 | if ($doFinallyBlock)
2302 | {
2303 | if ($pinnedArray -is [IDisposable]) { $pinnedArray.Dispose() }
2304 | }
2305 | }
2306 | }
2307 | catch
2308 | {
2309 | throw
2310 | }
2311 | finally
2312 | {
2313 | if ($passwordBytes -is [IDisposable]) { $passwordBytes.Dispose() }
2314 | }
2315 |
2316 | } # function Convert-PSCredentialToPinnedByteArray
2317 |
2318 | function Convert-ByteArrayToString
2319 | {
2320 | [CmdletBinding()]
2321 | [OutputType([string])]
2322 | param (
2323 | [Parameter(Mandatory = $true)]
2324 | [byte[]]
2325 | $ByteArray,
2326 |
2327 | [Parameter(Mandatory = $true)]
2328 | [UInt32]
2329 | $StartIndex,
2330 |
2331 | [Parameter(Mandatory = $true)]
2332 | [UInt32]
2333 | $ByteCount
2334 | )
2335 |
2336 | [System.Text.Encoding]::UTF8.GetString($ByteArray, $StartIndex, $ByteCount)
2337 | }
2338 |
2339 | function Convert-ByteArrayToSecureString
2340 | {
2341 | [CmdletBinding()]
2342 | [OutputType([System.Security.SecureString])]
2343 | param (
2344 | [Parameter(Mandatory = $true)]
2345 | [byte[]]
2346 | $ByteArray,
2347 |
2348 | [Parameter(Mandatory = $true)]
2349 | [UInt32]
2350 | $StartIndex,
2351 |
2352 | [Parameter(Mandatory = $true)]
2353 | [UInt32]
2354 | $ByteCount
2355 | )
2356 |
2357 | $chars = $null
2358 | $memoryStream = $null
2359 | $streamReader = $null
2360 |
2361 | try
2362 | {
2363 | $ss = New-Object System.Security.SecureString
2364 | $memoryStream = New-Object System.IO.MemoryStream($ByteArray, $StartIndex, $ByteCount)
2365 | $streamReader = New-Object System.IO.StreamReader($memoryStream, [System.Text.Encoding]::Unicode, $false)
2366 | $chars = New-Object PowerShellUtils.PinnedArray[char](1024)
2367 |
2368 | while (($read = $streamReader.Read($chars, 0, $chars.Count)) -gt 0)
2369 | {
2370 | for ($i = 0; $i -lt $read; $i++)
2371 | {
2372 | $ss.AppendChar($chars[$i])
2373 | }
2374 | }
2375 |
2376 | $ss.MakeReadOnly()
2377 | $ss
2378 | }
2379 | finally
2380 | {
2381 | if ($streamReader -is [IDisposable]) { $streamReader.Dispose() }
2382 | if ($memoryStream -is [IDisposable]) { $memoryStream.Dispose() }
2383 | if ($chars -is [IDisposable]) { $chars.Dispose() }
2384 | }
2385 |
2386 | } # function Convert-ByteArrayToSecureString
2387 |
2388 | function Convert-ByteArrayToPSCredential
2389 | {
2390 | [CmdletBinding()]
2391 | [OutputType([System.Management.Automation.PSCredential])]
2392 | param (
2393 | [Parameter(Mandatory = $true)]
2394 | [byte[]]
2395 | $ByteArray,
2396 |
2397 | [Parameter(Mandatory = $true)]
2398 | [UInt32]
2399 | $StartIndex,
2400 |
2401 | [Parameter(Mandatory = $true)]
2402 | [UInt32]
2403 | $ByteCount
2404 | )
2405 |
2406 | $message = 'Byte array is not a serialized PSCredential object.'
2407 |
2408 | if ($ByteCount -lt $script:PSCredentialHeader.Count + 4) { throw $message }
2409 |
2410 | for ($i = 0; $i -lt $script:PSCredentialHeader.Count; $i++)
2411 | {
2412 | if ($ByteArray[$StartIndex + $i] -ne $script:PSCredentialHeader[$i]) { throw $message }
2413 | }
2414 |
2415 | $i = $StartIndex + $script:PSCredentialHeader.Count
2416 |
2417 | $sizeBytes = $ByteArray[$i..($i+3)]
2418 | if (-not [System.BitConverter]::IsLittleEndian) { [array]::Reverse($sizeBytes) }
2419 |
2420 | $i += 4
2421 | $size = [System.BitConverter]::ToUInt32($sizeBytes, 0)
2422 |
2423 | if ($ByteCount -lt $i + $size) { throw $message }
2424 |
2425 | $userName = [System.Text.Encoding]::Unicode.GetString($ByteArray, $i, $size)
2426 | $i += $size
2427 |
2428 | try
2429 | {
2430 | $params = @{
2431 | ByteArray = $ByteArray
2432 | StartIndex = $i
2433 | ByteCount = $StartIndex + $ByteCount - $i
2434 | }
2435 | $secureString = Convert-ByteArrayToSecureString @params
2436 | }
2437 | catch
2438 | {
2439 | throw $message
2440 | }
2441 |
2442 | New-Object System.Management.Automation.PSCredential($userName, $secureString)
2443 |
2444 | } # function Convert-ByteArrayToPSCredential
2445 |
2446 | function Test-IsProtectedData
2447 | {
2448 | [CmdletBinding()]
2449 | [OutputType([bool])]
2450 | param (
2451 | [Parameter(Mandatory = $true)]
2452 | [psobject]
2453 | $InputObject
2454 | )
2455 |
2456 | $isValid = $true
2457 |
2458 | $cipherText = $InputObject.CipherText -as [byte[]]
2459 | $type = $InputObject.Type -as [string]
2460 |
2461 | if ($null -eq $cipherText -or $cipherText.Count -eq 0 -or
2462 | [string]::IsNullOrEmpty($type) -or
2463 | $null -eq $InputObject.KeyData)
2464 | {
2465 | $isValid = $false
2466 | }
2467 |
2468 | if ($isValid)
2469 | {
2470 | foreach ($object in $InputObject.KeyData)
2471 | {
2472 | if (-not (Test-IsKeyData -InputObject $object))
2473 | {
2474 | $isValid = $false
2475 | break
2476 | }
2477 | }
2478 | }
2479 |
2480 | return $isValid
2481 |
2482 | } # function Test-IsProtectedData
2483 |
2484 | function Test-IsKeyData
2485 | {
2486 | [CmdletBinding()]
2487 | [OutputType([bool])]
2488 | param (
2489 | [Parameter(Mandatory = $true)]
2490 | [psobject]
2491 | $InputObject
2492 | )
2493 |
2494 | $isValid = $true
2495 |
2496 | $key = $InputObject.Key -as [byte[]]
2497 | $iv = $InputObject.IV -as [byte[]]
2498 |
2499 | if ($null -eq $key -or $null -eq $iv -or $key.Count -eq 0 -or $iv.Count -eq 0)
2500 | {
2501 | $isValid = $false
2502 | }
2503 |
2504 | if ($isValid)
2505 | {
2506 | $isCertificate = Test-IsCertificateProtectedKeyData -InputObject $InputObject
2507 | $isPassword = Test-IsPasswordProtectedKeydata -InputObject $InputObject
2508 | $isValid = $isCertificate -or $isPassword
2509 | }
2510 |
2511 | return $isValid
2512 |
2513 | } # function Test-IsKeyData
2514 |
2515 | function Test-IsPasswordProtectedKeyData
2516 | {
2517 | [CmdletBinding()]
2518 | [OutputType([bool])]
2519 | param (
2520 | [Parameter(Mandatory = $true)]
2521 | [psobject]
2522 | $InputObject
2523 | )
2524 |
2525 | $isValid = $true
2526 |
2527 | $salt = $InputObject.Salt -as [byte[]]
2528 | $hash = $InputObject.Hash -as [string]
2529 | $hashSalt = $InputObject.HashSalt -as [byte[]]
2530 | $iterations = $InputObject.IterationCount -as [int]
2531 |
2532 | if ($null -eq $salt -or $salt.Count -eq 0 -or
2533 | $null -eq $hashSalt -or $hashSalt.Count -eq 0 -or
2534 | $null -eq $iterations -or $iterations -eq 0 -or
2535 | $null -eq $hash -or $hash -notmatch '^[A-F\d]+$')
2536 | {
2537 | $isValid = $false
2538 | }
2539 |
2540 | return $isValid
2541 |
2542 | } # function Test-IsPasswordProtectedKeyData
2543 |
2544 | function Test-IsCertificateProtectedKeyData
2545 | {
2546 | [CmdletBinding()]
2547 | [OutputType([bool])]
2548 | param (
2549 | [Parameter(Mandatory = $true)]
2550 | [psobject]
2551 | $InputObject
2552 | )
2553 |
2554 | $isValid = $true
2555 |
2556 | $thumbprint = $InputObject.Thumbprint -as [string]
2557 |
2558 | if ($null -eq $thumbprint -or $thumbprint -notmatch '^[A-F\d]+$')
2559 | {
2560 | $isValid = $false
2561 | }
2562 |
2563 | return $isValid
2564 |
2565 | } # function Test-IsCertificateProtectedKeyData
2566 |
2567 | #endregion
2568 |
--------------------------------------------------------------------------------