2

I'm trying to turn this MSDN page example code into a Powershell script.

The objective is to create files encrypted by a public key for transport into an environment with access to the private key so they can be decrypted.

I'm using New-SelfSignedCertificate to create the certificates because I don't need anyone to trust these certs, and I'm using the full certificate with public and private keys directly out of the Windows Key Store because I'm still just testing the code.

My problem is that when I encrypt, everything seems to work fine, but when decrypting I get the error message:

ERROR: Exception calling "Decrypt" with "2" argument(s): "The data to be decrypted exceeds the maximum for this modulus of 128 bytes."

It looks like New-SelfSignedCertificate is creating public keys of 2048 bits, and even though in the certs mmc snapin I can see that the cert has a private key, I am unable to see any of it's properties, either through UI or via code.

For instance the following code:

$cert = Get-Item 'Cert:\LocalMachine\AddressBook\<ThumbPrint>'
$cert.HasPrivateKey
$cert.PrivateKey

results in

True

and NULL

Here is the code

function ConvertTo-EncryptedFile
{
    [outputType([System.IO.FileInfo])]
    param
    (
        [parameter(Mandatory = $true)]
        [string]$path,
        [string]$client
    )

    $cert = Get-ClientCert -client $client

    if(Test-Path $path)
    {
        $file = Get-Item $path
        $folder = $file.DirectoryName
        $Name = $file.Name

        $destination = Join-Path $folder -ChildPath "$Name.encrypted"

        $serviceProvider = [System.Security.Cryptography.RSACryptoServiceProvider]$cert.PublicKey.Key
        $aesManaged = New-Object System.Security.Cryptography.AesManaged

        $aesManaged.KeySize = 256
        $aesManaged.BlockSize = 128
        $aesManaged.Mode = 'CBC'

        $transform = $aesManaged.CreateEncryptor()

        $keyformatter = New-Object System.Security.Cryptography.RSAPKCS1KeyExchangeformatter $serviceProvider

        [byte[]]$keyEncrypted = $keyformatter.CreateKeyExchange($aesManaged.Key, $aesManaged.GetType())

        [byte[]]$lenK = New-Object byte[] 4
        [byte[]]$lenIV = New-Object byte[] 4

        [int]$lKey = $keyEncrypted.Length
        $lenK = [System.BitConverter]::GetBytes($lKey)
        [int]$lIV = $aesManaged.IV.Length
        $lenIV = [System.BitConverter]::GetBytes($lIV)

        $outFS = New-Object System.IO.FileStream @($destination, [System.IO.FileMode]::Create)

        $outFS.Write($lenK, 0, 4)
        $outFS.Write($lenIV, 0, 4)
        $outFS.Write($keyEncrypted, 0, $lKey)
        $outFS.Write($aesManaged.IV, 0, $lIV)

        $outStreamEncrypted = New-Object System.Security.Cryptography.CryptoStream @($outFS, $transform, [System.Security.Cryptography.CryptoStreamMode]::Write)

        $count = 0
        $offset = 0

        $blockSizeBytes = $aesManaged.BlockSize / 8
        $data = New-Object byte[] $blockSizeBytes
        $bytesRead = 0

        $inFS = New-Object System.IO.FileStream @($path, [System.IO.FileMode]::Open)

        do
        {
            $count = $inFS.Read($data, 0, $blockSizeBytes)
            $offset += $count
            $outStreamEncrypted.Write($data, 0, $count)
            $bytesRead += $blockSizeBytes
        }
        while ($count -gt 0)
        $inFS.Close()
        $outStreamEncrypted.FlushFinalBlock()
        $outStreamEncrypted.Close()
        $outFS.Close()

        $inFS.Dispose()
        $outStreamEncrypted.Dispose()
        $outFS.Dispose()

        Remove-Variable transform
        $aesManaged.Dispose()

        Write-Output (Get-Item $destination)

    }
    else
    {
        throw "File to encrypt not found at path: $path"
    }

}

function ConvertFrom-EncryptedFile
{
    param
    (
        [parameter(Mandatory = $true)]
        [string]$path,
        [string]$client
    )

    $cert = Get-ClientCert -client $client

    if (Test-Path $path)
    {
        $destination = $path.Substring(0, $path.LastIndexOf('.'))
    }
    else
    {
        throw "File to decrypt not found at $path"
    }

    if ($cert.HasPrivateKey)
    {
        $rsaPrivateKey = New-Object System.Security.Cryptography.RSACryptoServiceProvider ($cert.PrivateKey)
    }

    $aesManaged = New-Object System.Security.Cryptography.AesManaged
    $aesManaged.KeySize = 256
    $aesManaged.BlockSize = 128
    $aesManaged.Mode = 'CBC'

    [byte[]]$lenK = New-Object System.Byte[] 4
    [byte[]]$lenIV = New-Object System.Byte[] 4

    [System.IO.FileStream]$inFs = New-Object System.IO.FileStream @($path, [System.IO.FileMode]::Open)

    $inFs.Seek(0, 'Begin')
    $inFs.Seek(0, 'Begin')

    $inFs.Read($lenK, 0, 3)

    $infs.Seek(4, 'Begin')

    $infs.Read($lenIV, 0, 3)

    [int]$lenK = [System.BitConverter]::ToInt32($lenK, 0)
    [int]$lenIV = [System.BitConverter]::ToInt32($lenIV, 0)

    [int]$startC = $lenK + $lenIV + 8
    [int]$lenC = [int]$inFs.Length - $startC

    [byte[]]$keyEncrypted = New-Object System.Byte[] $lenK
    [byte[]]$iv = New-Object System.Byte[] $lenIV

    $inFs.Seek(8, 'Begin')
    $inFs.Read($keyEncrypted, 0, $lenK)

    $inFs.Seek(8 + $lenK, 'Begin')
    $inFs.Read($iv, 0, $lenIV)

    [byte[]]$keyDecrypted = $rsaPrivateKey.Decrypt($keyEncrypted, $false)

}

It stops at decrypting the AES key because I haven't been able to get passed that hurdle yet.

I've tried reducing the AES key size from 256 to 128, but that didn't seem to work, and I don't really want to use a smaller key size anyway, I would rather figure out what's wrong with this code.

Thanks for any help!

Bill

briantist
  • 45,546
  • 6
  • 82
  • 127
Bill Hurt
  • 749
  • 1
  • 8
  • 26
  • Possible duplicate of [The data to be decrypted exceeds the maximum for this modulus of 128 bytes. RSA DECRYPTION c#](http://stackoverflow.com/questions/21604819/the-data-to-be-decrypted-exceeds-the-maximum-for-this-modulus-of-128-bytes-rsa) – briantist Mar 29 '16 at 21:47
  • @briantist I don't think it's a duplicate because I'm not trying to encrypt/Decrypt an entire file with RSA, only the AES key in the first few bytes of the file. – Bill Hurt Mar 29 '16 at 21:49
  • So the data are not longer than 128 bytes? – briantist Mar 29 '16 at 21:53
  • The data, as per the MSDN example, are 256 bytes. That data length should be ok since the encryption works, just not the decryption. I have not been able to modify the code in a way the results in a smaller key size either. – Bill Hurt Mar 29 '16 at 21:55
  • 1
    Although the encryption appears to work, the error seems to happen on decryption in the other question too, where the data size is the problem. [This answer to another question](http://stackoverflow.com/a/2477307/3905079) (linked from the first) offers an even better explanation. – briantist Mar 29 '16 at 22:03
  • `$inFs.Read($lenK, 0, 3)` Why `3` here? Why are you ignore return value? – user4003407 Mar 29 '16 at 22:07
  • I've read that one, and the theory makes sense, but it doesn't tell me what I can't create an AES key smaller than 256. To illustrate, after I execute $serviceProvider = [System.Security.Cryptography.RSACryptoServiceProvider]$cert.PublicKey.Key then call $serviceProvider.legalKeySizes I get Min Size 384 and maxSize 16384 – Bill Hurt Mar 29 '16 at 22:08
  • @PetSerAl 3 is to specify reading four bytes from an offset of the zero'th byte, if I'm reading it correctly. I ignore return value because the function read() on the object of type System.IO.FileStream uses the byte array $lenK to assign the output. The INT output from the function is just to denote how many bytes were read which I'm not concerned with. – Bill Hurt Mar 29 '16 at 22:09
  • How 3 means 4 bytes? Also it is valid to read less bytes then requested (although it likely never happens with `FileStream` unless you reach EOF), that is why you should not ignore return value of `Read` call. – user4003407 Mar 29 '16 at 22:19
  • @petserai 4 Bytes is just a guess based in the context of the code sample. Why would the code sample create a 4 byte array to read only 3 bytes? I don't know, maybe there's a good reason. I get that there's a purpose to that return value, but for the purposes of a demo with known file contents long enough to accommodate the requested read, it seems beside the point for the moment. – Bill Hurt Mar 29 '16 at 23:00
  • Do you really have `$cert.PrivateKey` `NULL` and `$cert.HasPrivateKey` `True`? Does `$cert.get_PrivateKey()` throw any exception? Probably problem with parameters, which you use for `New-SelfSignedCertificate`. Could you show them? – user4003407 Mar 30 '16 at 08:48
  • @PetSerAl Yes, $cert.get_privateKey() does throw an exception "Exception calling "get_PrivateKey" with "0" argument(s): "Invalid provider type specified." while $cert.get_publicKey() returns without error. The command I'm using to create the cert is "New-SelfSignedCertificate -DnsName $client -CertStoreLocation Cert:\LocalMachine\My" in Powershell 4.0 I'm starting to wonder if that command is simply creating a certificate that won't work for this purpose. – Bill Hurt Mar 30 '16 at 14:49
  • @PetSerAl Uh oh. From Certutil on my cert "Provider = Microsoft Software Key Storage Provider" vs other certs on the machine "Provider = Microsoft RSA SChannel Cryptographic Provider". I think the issue is a change int he crypto API on the certs that New-SelfSignedCertificate creates. I can't access the keys using the older method. Since you led me to the issue if you would like to post something to that effect I would be happy to mark it as the answer. – Bill Hurt Mar 30 '16 at 15:15

1 Answers1

1

It seems, the problems is that New-SelfSignedCertificate in PowerShell v4 choose provider, which is not appropriate to use with RSACryptoServiceProvider class, and does not have -Provider parameter, that allow specify provider explicitly.

One option to solve this would be to update to PowerShell v5. In PowerShell v5 New-SelfSignedCertificate cmdlet have -Provider parameter, so that you can specify desired provider:

PS> $Cert=New-SelfSignedCertificate -DnsName Test -CertStoreLocation Cert:\CurrentUser\My -Provider 'Microsoft Enhanced RSA and AES Cryptographic Provider'
PS> $Cert.PrivateKey


PublicOnly           : False
CspKeyContainerInfo  : System.Security.Cryptography.CspKeyContainerInfo
KeySize              : 2048
KeyExchangeAlgorithm : RSA-PKCS1-KeyEx
SignatureAlgorithm   : http://www.w3.org/2000/09/xmldsig#rsa-sha1
PersistKeyInCsp      : True
LegalKeySizes        : {System.Security.Cryptography.KeySizes}

To list installed providers you can use following commands:

PS> $Providers=New-Object -ComObject X509Enrollment.CCspInformations
PS> $Providers.AddAvailableCsps()
PS> $Providers|Format-Table Name,Type

Name                                                             Type
----                                                             ----
Microsoft Software Key Storage Provider                             0
Microsoft Passport Key Storage Provider                             0
Microsoft Smart Card Key Storage Provider                           0
Microsoft Base Cryptographic Provider v1.0                          1
Microsoft Base DSS and Diffie-Hellman Cryptographic Provider       13
Microsoft Base DSS Cryptographic Provider                           3
Microsoft Base Smart Card Crypto Provider                           1
Microsoft DH SChannel Cryptographic Provider                       18
Microsoft Enhanced Cryptographic Provider v1.0                      1
Microsoft Enhanced DSS and Diffie-Hellman Cryptographic Provider   13
Microsoft Enhanced RSA and AES Cryptographic Provider              24
Microsoft RSA SChannel Cryptographic Provider                      12
Microsoft Strong Cryptographic Provider                             1

As you need an RSA capable provider, then you need to choose provider with type 1, 12 or 24.

user4003407
  • 21,204
  • 4
  • 50
  • 60
  • Thanks for the extra bit about PS 5 and selecting the provider. I've been looking for a good reason to force the update and this looks like it. – Bill Hurt Mar 30 '16 at 17:16