Encrypt-File and Decrypt-File using X.509 Certificates

I like encrypting stuff.  Here are two Powershell functions I whipped up that can encrypt and decrypt a file using a valid key pair from an X.509 certificate. The encryption uses the public key, but only the corresponding private key can decrypt the data.

Encrypt-File:

#
<#
.SYNOPSIS
This Powershell function encrypts a file using a given X.509 certificate public key.
.DESCRIPTION
This Powershell function encrypts a file using a given X.509 certificate public key.
This function accepts as inputs a file to encrypt and a certificate with which to encrypt it.
This function saves the encrypted file as *.encrypted. The file can only be decrypted with the private key of the certificate that was used to encrypt it.
You must use a certificate that can be used for encryption, and not something like a code signing certificate.
.PARAMETER FileToEncrypt
Must be a System.IO.FileInfo object. $(Get-ChildItem C:\file.txt) will work.
.PARAMETER Cert
Must be a System.Security.Cryptography.X509Certificates.X509Certificate2 object. $(Get-ChildItem Cert:\CurrentUser\My\9554F368FEA619A655A1D49408FC13C3E0D60E11) will work. The public key of the certificate is used for encryption.
.EXAMPLE
PS C:\> . .\Encrypt-File.ps1
PS C:\> Encrypt-File $File $Cert
.EXAMPLE
PS C:\> . .\Encrypt-File.ps1
PS C:\> Encrypt-File $(Get-ChildItem C:\foo.txt) $(Get-ChildItem Cert:\CurrentUser\My\THUMBPRINT)
.INPUTS
Encrypt-File <System.IO.FileInfo> <System.Security.Cryptography.X509Certificates.X509Certificate2>
.OUTPUTS
A file named $FileName.encrypted
.NOTES
Written by Ryan Ries - ryan@myotherpcisacloud.com
.LINK
http://msdn.microsoft.com/en-us/library/system.security.cryptography.x509certificates.x509certificate2.aspx
#>

Function Encrypt-File
{
	Param([Parameter(mandatory=$true)][System.IO.FileInfo]$FileToEncrypt,
		  [Parameter(mandatory=$true)][System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert)

	Try { [System.Reflection.Assembly]::LoadWithPartialName("System.Security.Cryptography") }
	Catch { Write-Error "Could not load required assembly."; Return }	
	
	$AesProvider                = New-Object System.Security.Cryptography.AesManaged
	$AesProvider.KeySize        = 256
	$AesProvider.BlockSize      = 128
	$AesProvider.Mode           = [System.Security.Cryptography.CipherMode]::CBC
	$KeyFormatter               = New-Object System.Security.Cryptography.RSAPKCS1KeyExchangeFormatter($Cert.PublicKey.Key)
	[Byte[]]$KeyEncrypted       = $KeyFormatter.CreateKeyExchange($AesProvider.Key, $AesProvider.GetType())
	[Byte[]]$LenKey             = $Null
	[Byte[]]$LenIV              = $Null
	[Int]$LKey                  = $KeyEncrypted.Length
	$LenKey                     = [System.BitConverter]::GetBytes($LKey)
	[Int]$LIV                   = $AesProvider.IV.Length
	$LenIV                      = [System.BitConverter]::GetBytes($LIV)
	$FileStreamWriter           
	Try { $FileStreamWriter = New-Object System.IO.FileStream("$($FileToEncrypt.FullName)`.encrypted", [System.IO.FileMode]::Create) }
	Catch { Write-Error "Unable to open output file for writing."; Return }
	$FileStreamWriter.Write($LenKey,         0, 4)
	$FileStreamWriter.Write($LenIV,          0, 4)
	$FileStreamWriter.Write($KeyEncrypted,   0, $LKey)
	$FileStreamWriter.Write($AesProvider.IV, 0, $LIV)
	$Transform                  = $AesProvider.CreateEncryptor()
	$CryptoStream               = New-Object System.Security.Cryptography.CryptoStream($FileStreamWriter, $Transform, [System.Security.Cryptography.CryptoStreamMode]::Write)
	[Int]$Count                 = 0
	[Int]$Offset                = 0
	[Int]$BlockSizeBytes        = $AesProvider.BlockSize / 8
	[Byte[]]$Data               = New-Object Byte[] $BlockSizeBytes
	[Int]$BytesRead             = 0
	Try { $FileStreamReader     = New-Object System.IO.FileStream("$($FileToEncrypt.FullName)", [System.IO.FileMode]::Open)	}
	Catch { Write-Error "Unable to open input file for reading."; Return }
	Do
	{
		$Count   = $FileStreamReader.Read($Data, 0, $BlockSizeBytes)
		$Offset += $Count
		$CryptoStream.Write($Data, 0, $Count)
		$BytesRead += $BlockSizeBytes
	}
	While ($Count -gt 0)
	
	$CryptoStream.FlushFinalBlock()
	$CryptoStream.Close()
	$FileStreamReader.Close()
	$FileStreamWriter.Close()
}

Decrypt-File:

#
<#
.SYNOPSIS
This Powershell function decrypts a file using a given X.509 certificate private key.
.DESCRIPTION
This Powershell function decrypts a file using a given X.509 certificate private key.
This function accepts as inputs a file to decrypt and a certificate with which to decrypt it.
The file can only be decrypted with the private key of the certificate that was used to encrypt it.
.PARAMETER FileToDecrypt
Must be a System.IO.FileInfo object. $(Get-ChildItem C:\file.txt) will work.
.PARAMETER Cert
Must be a System.Security.Cryptography.X509Certificates.X509Certificate2 object. $(Get-ChildItem Cert:\CurrentUser\My\9554F368FEA619A655A1D49408FC13C3E0D60E11) will work. The public key of the certificate is used for encryption. The private key is used for decryption.
.EXAMPLE
PS C:\> . .\Decrypt-File.ps1
PS C:\> Decrypt-File $File $Cert
.EXAMPLE
PS C:\> . .\Decrypt-File.ps1
PS C:\> Decrypt-File $(Get-ChildItem C:\foo.txt) $(Get-ChildItem Cert:\CurrentUser\My\THUMBPRINT)
.INPUTS
Decrypt-File <System.IO.FileInfo> <System.Security.Cryptography.X509Certificates.X509Certificate2>
.OUTPUTS
An unencrypted file.
.NOTES
Written by Ryan Ries - ryan@myotherpcisacloud.com
.LINK
http://msdn.microsoft.com/en-us/library/system.security.cryptography.x509certificates.x509certificate2.aspx
#>

Function Decrypt-File
{
	Param([Parameter(mandatory=$true)][System.IO.FileInfo]$FileToDecrypt,
		  [Parameter(mandatory=$true)][System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert)

	Try { [System.Reflection.Assembly]::LoadWithPartialName("System.Security.Cryptography") }
	Catch { Write-Error "Could not load required assembly."; Return }
	
	$AesProvider                = New-Object System.Security.Cryptography.AesManaged
	$AesProvider.KeySize        = 256
	$AesProvider.BlockSize      = 128
	$AesProvider.Mode           = [System.Security.Cryptography.CipherMode]::CBC
	[Byte[]]$LenKey             = New-Object Byte[] 4
	[Byte[]]$LenIV              = New-Object Byte[] 4
	If($FileToDecrypt.Name.Split(".")[-1] -ne "encrypted")
	{
		Write-Error "The file to decrypt must be named *.encrypted."
		Return
	}
	If($Cert.HasPrivateKey -eq $False -or $Cert.PrivateKey -eq $null)
	{
		Write-Error "The supplied certificate does not contain a private key, or it could not be accessed."
		Return
	}
	Try { $FileStreamReader = New-Object System.IO.FileStream("$($FileToDecrypt.FullName)", [System.IO.FileMode]::Open)	}
	Catch
	{ 
		Write-Error "Unable to open input file for reading."		
		Return 
	}	
	$FileStreamReader.Seek(0, [System.IO.SeekOrigin]::Begin)         | Out-Null
	$FileStreamReader.Seek(0, [System.IO.SeekOrigin]::Begin)         | Out-Null
	$FileStreamReader.Read($LenKey, 0, 3)                            | Out-Null
	$FileStreamReader.Seek(4, [System.IO.SeekOrigin]::Begin)         | Out-Null
	$FileStreamReader.Read($LenIV,  0, 3)                            | Out-Null
	[Int]$LKey            = [System.BitConverter]::ToInt32($LenKey, 0)
	[Int]$LIV             = [System.BitConverter]::ToInt32($LenIV,  0)
	[Int]$StartC          = $LKey + $LIV + 8
	[Int]$LenC            = [Int]$FileStreamReader.Length - $StartC
	[Byte[]]$KeyEncrypted = New-Object Byte[] $LKey
	[Byte[]]$IV           = New-Object Byte[] $LIV
	$FileStreamReader.Seek(8, [System.IO.SeekOrigin]::Begin)         | Out-Null
	$FileStreamReader.Read($KeyEncrypted, 0, $LKey)                  | Out-Null
	$FileStreamReader.Seek(8 + $LKey, [System.IO.SeekOrigin]::Begin) | Out-Null
	$FileStreamReader.Read($IV, 0, $LIV)                             | Out-Null
	[Byte[]]$KeyDecrypted = $Cert.PrivateKey.Decrypt($KeyEncrypted, $false)
	$Transform = $AesProvider.CreateDecryptor($KeyDecrypted, $IV)
	Try	{ $FileStreamWriter = New-Object System.IO.FileStream("$($FileToDecrypt.Directory)\$($FileToDecrypt.Name.Replace(".encrypted",$null))", [System.IO.FileMode]::Create) }
	Catch 
	{ 
		Write-Error "Unable to open output file for writing.`n$($_.Message)"
		$FileStreamReader.Close()
		Return
	}
	[Int]$Count  = 0
	[Int]$Offset = 0
	[Int]$BlockSizeBytes = $AesProvider.BlockSize / 8
	[Byte[]]$Data = New-Object Byte[] $BlockSizeBytes
	$CryptoStream = New-Object System.Security.Cryptography.CryptoStream($FileStreamWriter, $Transform, [System.Security.Cryptography.CryptoStreamMode]::Write)
	Do
	{
		$Count   = $FileStreamReader.Read($Data, 0, $BlockSizeBytes)
		$Offset += $Count
		$CryptoStream.Write($Data, 0, $Count)
	}
	While ($Count -gt 0)
	$CryptoStream.FlushFinalBlock()
	$CryptoStream.Close()
	$FileStreamWriter.Close()
	$FileStreamReader.Close()
}

Comments (7) -

Lukasz Duczmal 5/10/2013 9:19:34 AM

Hi,
these are very interesting scripts - could you tell me a little bit more about your code, please.
I try to encrypt not files but strings (I plan to use some external file as a setup set for script and some entries in this setup file should be encrypted). I created simple function which use X509 certificates to encrypt and decrypt a string. It works quite well but one need private keys for both encrypting and decrypting. it means it is symmetrical cryptography (I think). I would like to use asymmetrical cryptography: what is encrypted by private key could be decrypted by public key and vice-versa. I also wonder whether is it possible to detect which certificate was used a file encrypted by your script.
Best regards
Ɓukasz

Hi, thank you for commenting.

The general idea is that symmetric encryption/decryption is computationally fast, while asymmetric encryption/decryption is relatively hard/slow.

So you use asymmetric encryption, as implemented with the public and private keys of X.509 certificates, to encrypt a very small piece of data, since asymmetric encryption is slow.  That small piece of data is the key that will be used in a symmetric algorithm.  You then use that key to encrypt a large amount of data with a relatively fast symmetric algorithm, such as AES.  You can transfer the key to the intended recipient securely because it's encrypted with the asymmetric encryption algorithm provided by the certificate.

So in steps, imagine this:

1. I encrypt a secret key using the public key of the X.509 certificate of the intended recipient of my message.

2. I use that key to symmetrically encrypt a large block of data.

3. I send the asymmetrically encrypted key and the symmetrically encrypted data to the recipient.

4. The recipient decrypts the key using the private key of his certificate.

5. He then uses that decrypted key to unlock the block of data I sent.

The System.Security.Cryptography Namespace will technically allow you to encrypt data symmetrically using a certificate's private key or public key, but that is not the way the process is really intended to work.  That is why you will notice that the method will generate an exception if you try to encrypt a block of data that is larger than the key itself (e.g. 1024 or 2048 bits.)  Because the certificate is actually only meant to encrypt a very small piece of data.

Ryan, This code is awesome. Thanks for posting it.
One question is: How do I take a normal certificate (self-signed cert saved to PFX) and export /just/ a Public Key certificate and (also) /just/ a Private Key certificate? I've been looking around, but I cant seem to goole-code a solution which would produce the x.509 certificates used in your solution above.

ThxAgn,  -TheJonnyG

Hey JonnyG, thanks for commenting.

To get an object of the type System.Security.Cryptography.X509Certificates.X509Certificate2, you can easily import them from a PFX using the X509Certificate2Collection.Import(pfxFile, pfxPassword, keyStorageFlags) method, or, you can import one from your Windows certificate store using something like this:

X509Store store = new X509Store("My");
store.Open(OpenFlags.ReadOnly);
foreach (X509Certificate2 mCert in store.Certificates)
{  
    // Do stuff
}

Ryan,
Thanks for the help, but I was looking for a way to export JUST the public (or private) keys from a PFX, I think I figured it out for the Public Keys, but still cant find a way to export a windows-importable certificate that contains JUST the Private keys.
However if there's no way to export Just the private keys to a cert and have windows able to use it within the CERT:\ store, i'll just use the PFX itself (which contains the private keys). And use the below function to create a publicly distributable PUBLIC key to encrypt files with. Thanks again for publishing the above functions.


Function ExportPublicKeyFromPFX(){
  Param(
    [string]pfxFilepath,
    [string]pfxPassword,
    [string]PublicKeyFilename=".\PublicKey.cer"
  );
  $pfx = new-object System.Security.Cryptography.X509Certificates.X509Certificate2;
  $pfx.import("$pfxFilepath",$pfxpassword,"Exportable,PersistKeySet");
  $PublicKeys = $pfx.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert);
  [system.io.file]::writeallbytes("$PublicKeyFilename",$PublicKeys);
}

i need c code for symmetric encrypting and decrypting a string. can anyone help me?

When I'm trying to decrypt a file, I'm getting an error: exception in call of "Decrypt" - "bad key" (it's translation to english) =\ Could anyone help me, please?

Comments are closed