· 5 years ago · Jan 28, 2021, 11:46 AM
1# Licensed under the Apache License, Version 2.0 (the "License");
2# you may not use this file except in compliance with the License.
3# You may obtain a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS,
9# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10# See the License for the specific language governing permissions and
11# limitations under the License.
12
13filter Unprotect-String
14{
15 <#
16 .SYNOPSIS
17 Decrypts a string.
18
19 .DESCRIPTION
20 `Unprotect-String` decrypts a string encrypted via the Data Protection API (DPAPI) or RSA. It uses the DP/RSA APIs to decrypted the secret into an array of bytes, which is then converted to a UTF8 string. Beginning with Carbon 2.0, after conversion, the decrypted array of bytes is cleared in memory.
21
22 Also beginning in Carbon 2.0, use the `AsSecureString` switch to cause `Unprotect-String` to return the decrypted string as a `System.Security.SecureString`, thus preventing your secret from hanging out in memory. When converting to a secure string, the secret is decrypted to an array of bytes, and then converted to an array of characters. Each character is appended to the secure string, after which it is cleared in memory. When the conversion is complete, the decrypted byte array is also cleared out in memory.
23
24 `Unprotect-String` can decrypt using the following techniques.
25
26 ## DPAPI
27
28 This is the default. The string must have also been encrypted with the DPAPI. The string must have been encrypted at the current user's scope or the local machien scope.
29
30 ## RSA
31
32 RSA is an assymetric encryption/decryption algorithm, which requires a public/private key pair. This method decrypts a secret that was encrypted with the public key using the private key.
33
34 You can specify the private key in three ways:
35
36 * with a `System.Security.Cryptography.X509Certificates.X509Certificate2` object, via the `Certificate` parameter
37 * with a certificate in one of the Windows certificate stores, passing its unique thumbprint via the `Thumbprint` parameter, or via the `PrivateKeyPath` parameter, which can be a certificat provider path, e.g. it starts with `cert:\`.
38 * with an X509 certificate file, via the `PrivateKeyPath` parameter
39
40 .LINK
41 New-RsaKeyPair
42
43 .LINK
44 Protect-String
45
46 .LINK
47 http://msdn.microsoft.com/en-us/library/system.security.cryptography.protecteddata.aspx
48
49 .EXAMPLE
50 PS> $password = Unprotect-String -ProtectedString $encryptedPassword
51
52 Decrypts a protected string which was encrypted at the current user or default scopes using the DPAPI. The secret must have been encrypted at the current user's scope or at the local computer's scope.
53
54 .EXAMPLE
55 Protect-String -String 'NotSoSecretSecret' -ForUser | Unprotect-String
56
57 Demonstrates how Unprotect-String takes input from the pipeline. Adds 'NotSoSecretSecret' to the pipeline.
58
59 .EXAMPLE
60 Unprotect-String -ProtectedString $ciphertext -Certificate $myCert
61
62 Demonstrates how to encrypt a secret using RSA with a `System.Security.Cryptography.X509Certificates.X509Certificate2` object. You're responsible for creating/loading it. The `New-RsaKeyPair` function will create a key pair for you, if you've got a Windows SDK installed.
63
64 .EXAMPLE
65 Unprotect-String -ProtectedString $ciphertext -Thumbprint '44A7C27F3353BC53F82318C14490D7E2500B6D9E'
66
67 Demonstrates how to decrypt a secret using RSA with a certificate in one of the Windows certificate stores. All local machine and user stores are searched. The current user must have permission/access to the certificate's private key.
68
69 .EXAMPLE
70 Unprotect -ProtectedString $ciphertext -PrivateKeyPath 'C:\Projects\Security\publickey.cer'
71
72 Demonstrates how to encrypt a secret using RSA with a certificate file. The file must be loadable by the `System.Security.Cryptography.X509Certificates.X509Certificate` class.
73
74 .EXAMPLE
75 Unprotect -ProtectedString $ciphertext -PrivateKeyPath 'cert:\LocalMachine\My\44A7C27F3353BC53F82318C14490D7E2500B6D9E'
76
77 Demonstrates how to encrypt a secret using RSA with a certificate in the store, giving its exact path.
78 #>
79 [CmdletBinding(DefaultParameterSetName='DPAPI')]
80 param(
81 [Parameter(Mandatory = $true, Position=0, ValueFromPipeline = $true)]
82 [string]
83 # The text to decrypt.
84 $ProtectedString,
85
86 [Parameter(Mandatory=$true,ParameterSetName='RSAByCertificate')]
87 [Security.Cryptography.X509Certificates.X509Certificate2]
88 # The private key to use for decrypting.
89 $Certificate,
90
91 [Parameter(Mandatory=$true,ParameterSetName='RSAByThumbprint')]
92 [string]
93 # The thumbprint of the certificate, found in one of the Windows certificate stores, to use when decrypting. All certificate stores are searched. The current user must have permission to the private key.
94 $Thumbprint,
95
96 [Parameter(Mandatory=$true,ParameterSetName='RSAByPath')]
97 [string]
98 # The path to the private key to use for encrypting. Must be to an `X509Certificate2` file or a certificate in a certificate store.
99 $PrivateKeyPath,
100
101 [Parameter(ParameterSetName='RSAByPath')]
102 # The password for the private key, if it has one. It really should. Can be a `[string]` or a `[securestring]`.
103 $Password,
104
105 [Parameter(ParameterSetName='RSAByCertificate')]
106 [Parameter(ParameterSetName='RSAByThumbprint')]
107 [Parameter(ParameterSetName='RSAByPath')]
108 [Switch]
109 # If true, uses Direct Encryption (PKCS#1 v1.5) padding. Otherwise (the default), uses OAEP (PKCS#1 v2) padding. See [Encrypt](http://msdn.microsoft.com/en-us/library/system.security.cryptography.rsacryptoserviceprovider.encrypt(v=vs.110).aspx) for information.
110 $UseDirectEncryptionPadding,
111
112 [Switch]
113 # Returns the unprotected string as a secure string. The original decrypted bytes are zeroed out to limit the memory exposure of the decrypted secret, i.e. the decrypted secret will never be in a `string` object.
114 $AsSecureString
115 )
116
117 Set-StrictMode -Version 'Latest'
118
119 Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState
120
121 $encryptedBytes = [Convert]::FromBase64String($ProtectedString)
122 if( $PSCmdlet.ParameterSetName -eq 'DPAPI' )
123 {
124 $decryptedBytes = [Security.Cryptography.ProtectedData]::Unprotect( $encryptedBytes, $null, 0 )
125 }
126 elseif( $PSCmdlet.ParameterSetName -like 'RSA*' )
127 {
128 if( $PSCmdlet.ParameterSetName -like '*ByPath' )
129 {
130 $passwordParam = @{ }
131 if( $Password )
132 {
133 $passwordParam = @{ Password = $Password }
134 }
135 $Certificate = Get-Certificate -Path $PrivateKeyPath @passwordParam
136 if( -not $Certificate )
137 {
138 return
139 }
140 }
141 elseif( $PSCmdlet.ParameterSetName -like '*ByThumbprint' )
142 {
143 $certificates = Get-ChildItem -Path ('cert:\*\*\{0}' -f $Thumbprint) -Recurse
144 if( -not $certificates )
145 {
146 Write-Error ('Certificate ''{0}'' not found.' -f $Thumbprint)
147 return
148 }
149
150 $Certificate = $certificates | Where-Object { $_.HasPrivateKey } | Select-Object -First 1
151 if( -not $Certificate )
152 {
153 Write-Error ('Certificate ''{0}'' ({1}) doesn''t have a private key.' -f $certificates[0].Subject, $Thumbprint)
154 return
155 }
156 }
157
158 if( -not $Certificate.HasPrivateKey )
159 {
160 Write-Error ('Certificate ''{0}'' ({1}) doesn''t have a private key. When decrypting with RSA, secrets are encrypted with the public key, and decrypted with a private key.' -f $Certificate.Subject,$Certificate.Thumbprint)
161 return
162 }
163
164 if( -not $Certificate.PrivateKey )
165 {
166 Write-Error ('Certificate ''{0}'' ({1}) has a private key, but it is currently null or not set. This usually means your certificate was imported or generated incorrectly. Make sure you''ve generated an RSA public/private key pair and are using the private key. If the private key is in the Windows certificate stores, make sure it was imported correctly (`Get-ChildItem $pathToCert | Select-Object -Expand PrivateKey` isn''t null).' -f $Certificate.Subject,$Certificate.Thumbprint)
167 return
168 }
169
170 [Security.Cryptography.RSACryptoServiceProvider]$privateKey = $null
171 if( $Certificate.PrivateKey -isnot [Security.Cryptography.RSACryptoServiceProvider] )
172 {
173 Write-Error ('Certificate ''{0}'' (''{1}'') is not an RSA key. Found a private key of type ''{2}'', but expected type ''{3}''.' -f $Certificate.Subject,$Certificate.Thumbprint,$Certificate.PrivateKey.GetType().FullName,[Security.Cryptography.RSACryptoServiceProvider].FullName)
174 return
175 }
176
177 try
178 {
179 $privateKey = $Certificate.PrivateKey
180 $decryptedBytes = $privateKey.Decrypt( $encryptedBytes, (-not $UseDirectEncryptionPadding) )
181 }
182 catch
183 {
184 if( $_.Exception.Message -match 'Error occurred while decoding OAEP padding' )
185 {
186 [int]$maxLengthGuess = ($privateKey.KeySize - (2 * 160 - 2)) / 8
187 Write-Error (@'
188Failed to decrypt string using certificate '{0}' ({1}). This can happen when:
189 * The string to decrypt is too long because the original string you encrypted was at or near the maximum allowed by your key's size, which is {2} bits. We estimate the maximum string size you can encrypt is {3} bytes. You may get this error even if the original encrypted string is within a couple bytes of that maximum.
190 * The string was encrypted with a different key
191 * The string isn't encrypted
192
193{4}: {5}
194'@ -f $Certificate.Subject, $Certificate.Thumbprint,$privateKey.KeySize,$maxLengthGuess,$_.Exception.GetType().FullName,$_.Exception.Message)
195 return
196 }
197 elseif( $_.Exception.Message -match '(Bad Data|The parameter is incorrect)\.' )
198 {
199 Write-Error (@'
200Failed to decrypt string using certificate '{0}' ({1}). This usually happens when the padding algorithm used when encrypting/decrypting is different. Check the `-UseDirectEncryptionPadding` switch is the same for both calls to `Protect-String` and `Unprotect-String`.
201
202{2}: {3}
203'@ -f $Certificate.Subject,$Certificate.Thumbprint,$_.Exception.GetType().FullName,$_.Exception.Message)
204 return
205 }
206 Write-Error -Exception $_.Exception
207 return
208 }
209 }
210
211 try
212 {
213 if( $AsSecureString )
214 {
215 $secureString = New-Object 'Security.SecureString'
216 [char[]]$chars = [Text.Encoding]::UTF8.GetChars( $decryptedBytes )
217 for( $idx = 0; $idx -lt $chars.Count ; $idx++ )
218 {
219 $secureString.AppendChar( $chars[$idx] )
220 $chars[$idx] = 0
221 }
222
223 $secureString.MakeReadOnly()
224 return $secureString
225 }
226 else
227 {
228 [Text.Encoding]::UTF8.GetString( $decryptedBytes )
229 }
230 }
231 finally
232 {
233 [Array]::Clear( $decryptedBytes, 0, $decryptedBytes.Length )
234 }
235}