Certificates, part 3: encryption and decryption by hand, and SecureString
Continuing the example from part 2, what if you don't have the class EnvelopedCms, such as on the NanoServer in general and CoreCLR in particular? (BTW, that class will be added in the final server 2016 release but it's not available in the current preview). Then you can construct the envelopes manually. In the simple case if both the encryption and decryption are done by your code, you don't have to follow the standard envelopes and can make your own simplified envelopes.
I've found an example on the internet and modified it:
function Protect-CustomString
{
<#
.SYNOPSIS
Encrypt the contents of a string on a certificate, manually constructing an envelope.
.OUTPUT
Either the encoded bytes or the Base64 string with them.
#>
param(
## String to encrypt
[AllowEmptyString()]
[AllowEmptyCollection()]
[Parameter(Mandatory=$true)]
[string] $String,
## Cert to encrypt with the public key.
[Parameter(Mandatory=$true)]
[System.Security.Cryptography.X509Certificates.X509Certificate2] $Cert,
## Return the result as a Base64 string. Otherwise as bytes.
[switch] $Base64
)
$sedata = ConvertTo-SecureString -Force -AsPlainText $String
$key = New-Object byte[](32)
$rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::Create()
$rng.GetBytes($key)
# Payload is Base64-encoded
$payload = ConvertFrom-SecureString -Key $key $sedata
# Restore it back to bytes
$paybytes = [Convert]::FromBase64String($payload)
# Encrypt the one-time key with the public key.
# $true selects the newer padding mode
$enkey = $cert.PublicKey.Key.Encrypt($key, $true)
# Encode the length of the key as bytes.
$len = @([byte]($enkey.count -band 0xFF), [byte]($enkey.count -shr 8))
Write-Verbose "Symmetric key length is $($enkey.count) bytes"
# Convert the thumbprint to bytes.
$thumbytes = New-Object byte[](20)
$thumb = $Cert.Thumbprint
for ($i = 0; $i -lt $thumb.Length; $i += 2) {
$thumbytes[$i/2] = [byte]"0x$($thumb.Substring($i,2))"
}
$bytes = $thumbytes + $len + $enkey + $paybytes
if ($Base64) {
[System.Convert]::ToBase64String($bytes)
} else {
$bytes
}
}
function Unprotect-CustomString
{
<#
.SYNOPSIS
Decrypt a bunch of bytes on a certificate and decode them into a string.
The same certificate with the
private key must be already installed locally, the decryption will
find it by the thumbprint in the envelope.
.OUTPUT
The decoded string.
#>
[CmdletBinding()]
param(
## Bytes to decrypt.
[Parameter(ParameterSetName = "Bytes", Mandatory=$true)]
[byte[]] $Bytes,
## Another option: Base64 string to decrypt.
[Parameter(ParameterSetName = "String", Mandatory=$true)]
[string] $String,
## Force a particular certificate. By default the cert will be found
## by thumbprint.
[System.Security.Cryptography.X509Certificates.X509Certificate2] $Cert
)
if ($PsCmdlet.ParameterSetName -eq "String") {
$Bytes = [Convert]::FromBase64String($String)
}
if ($Cert) {
if (!$Cert.PrivateKey) {
throw "Explicit certificate with thumbprint $($Cert.Thumbprint) has a null property PrivateKey. Try using a cert with a Crypto provider, not Key Store provider."
}
} else {
$thumbytes = $Bytes[0..19]
$thumb = ""
foreach ($b in $thumbytes) {
$thumb += "{0:X2}" -f $b
}
Write-Verbose "Thumbprint is $thumb"
$Cert = @(dir -Recurse cert:\ | ? { $_.Thumbprint -eq $thumb -and $_.PrivateKey})[0]
if (!$Cert) {
throw "Cannot find a certificate with thumbprint $thumb and property PrivateKey in it not null. Try using a cert with a Crypto provider, not Key Store provider."
}
}
Write-Verbose "Using the certificate $($cert.PSPath)"
$keylen = [uint32]$Bytes[20] + ([uint32]$Bytes[21] -shl 8)
Write-Verbose "Symmetric key length is $keylen bytes"
$enkey = $Bytes[22..(22-1+$keylen)]
$payload = [System.Convert]::ToBase64String( $Bytes[(22+$keylen)..($Bytes.Count)] )
# $true selects the newer padding mode
$key = $cert.PrivateKey.Decrypt($enkey, $true)
$sedata = ConvertTo-SecureString -Key $key $payload
# Convert the SecureString to plain text.
(New-Object System.Net.NetworkCredential "",$sedata).Password
}
Here is an example of its use:
PS C:\WINDOWS\system32> $encbytes = Protect-CustomString -String "abcdefg" -Cert $mycert
PS C:\WINDOWS\system32> Unprotect-CustomString -Bytes $encbytes
abcdefg
It can also encode the encrypted string in Base 64:
PS C:\WINDOWS\system32> $encs = Protect-CustomString -String "abcdefg" -Cert $mycert -Base64
PS C:\WINDOWS\system32> $encs
3Zgx++Xfz4SGvVFIKF82EBSlXYIAARBIPE5p2FnGfaXGCR/OU/CeSYzT8Ae+uXHYKRU3I2avk1lfbKJfE9Hv7JaYwOB71SvB+Vuu+YYE1B9iYeA0a1qfeFQ
n7A0HDBS4R16QttFUBLfELvOm95mOV4YEajhwZPiHjDnUTTvUXi9yyixB8RWc5kaLQo7SkJkkwsHgIdHej7QBc1omcSTS/e9E3UVydrS7VkeM708k6P38Tc
y6TS4skGDOPXcPs/idaW2Uv530z/QxMbb50OWxWI7IX8VB8kJy6z+//Odve+BldT6X9eYsNl2kxVvNFpiauBqTYWN18DddfqbCbzxemqRgjdbHuE1RWzqm4
YcQj3IA9+N36Kfvrj3Z3XXXrvjd/TjbfjXdvXrTnRrnfjkyAHwAcABBAFYATgBlAEkAcQBEAE0AQQBHAGkAOABQAHQAVABwAGcAdQBBADYAZwA9AD0AfAAw
AGMAZgA4ADQAMAA4ADYAYQBiADcANABlADgAZgA5ADkAOAA4ADUAMwA0ADcAMQBiAGYAMwA1AGIANQBlADcA
PS C:\WINDOWS\system32> Unprotect-CustomString -String $encs
abcdefg
Let's look at what's inside the functions. The encryption with the symmetric AES algorithm is done with the help of a SecureString object. The key for that encryption is generated as a random number from the cryptographic-grade random number generator. Then the thumbprint of the certificate, the AES key, and the payload are assembled into a simple envelope.
SecureString is a weird thing. Its idea is that you'd store the sensitive information like passwords in them, so that they won't be easily visible. And then you use them to construct the credential objects. You can even write them into the files and read back without exposing the contents by using ConvertFrom-SecureString, they will be encrypted while in the file. What key is used for that encryption? The default key is derived from the user account's key. The consequence is that if you export the secure string with basic ConvertFrom-SecureString from one account and then import from a different account, you'll get garbage. But ConvertFrom-SecureString also allows you to specify an explicit AES key instead, and then you'll be able to parse the data back using the same key.
The last part is to extract the original string back from the SecureString. It's obviously possible and very useful but there is no straightforward way to do it. I guess they want to discourage this kind of use. But I've found some very nice hacks on Stackoverflow. I must say that one thing that really annoys me about Windows is that way to often there is no straightforward way to do some useful and straightforward thing. Instead of that the hacks are invented and spread through the Internet. I wonder, how did people program on Windows before Stackoverflow and before the search engines became widespread.
The more proper way to do it is how I suppose it gets extracted inside the code that deals with credentials and such:
ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToGlobalAllocUnicode($ss)
$plain = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($ptr)
[System.Runtime.InteropServices.Marshal]::ZeroFreeGlobalAllocUnicode($ptr)
Why not make it a simple method on the SecureString object is a total mystery to me.
The other way is a hack that abuses an unrelated object but is a very short and neat one:
$plain = (New-Object System.Net.NetworkCredential "",$ss).Password
Now you have it.