├── 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:__ [![Build status](https://build.powershell.org/guestAuth/app/rest/builds/buildType:(id:ProtectedData_PublishStatusToGitHub)/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 | --------------------------------------------------------------------------------