Custom Certificate Based File Encryption

The Problem

I was recently asked to come up with a method to ensure files are encrypted at rest after they are transferred from client servers onto my employers servers. We have constraints on the way we do things that can make it difficult to install third party software to get things like this done, so I often find myself in the position of having to come up with my own code to do this kind of thing. That being said, cryptographer is hard, even for really smart people, so I have no intention of actually rolling my own crypto.

Thankfully, with Powershell, it’s turn the .NET crypto classes into a usable tool for my purposes. Over the next few blog posts I’ll be using this MSDN page as the basis for a custom encryption module that can be deployed to servers without having to install any third party software at all.

How Certificate Based Cryptography is Going to Work

Since you’re reading a blog mostly about Powershell scripting, I’m going to assume you are working in a Windows environment. The most natural way then to encrypt and decrypt files will be using certificates managed in the Windows Certificate store. The thing is, if you know how encrypting an SSL web session works, Public/Private key cryptography isn’t actually very good for encrypting large amounts of data like text files or media files.

If you read the linked MSDN page closely you get something like the following workflow.

  1. Create a certificate with a public and private key pair
  2. Export the public key and copy to the server the source sensitive files
  3. For each file that we encrypt we will do the following
    1. Generate a new AES symetric encryption key and IV
    2. Use the key and IV to encrypt the data file.
    3. Use the public key to encrypt the key and the IV and prepend them to the encrypted data file.
  4. When the files arrive on the destination server we can use the private key decrypt the key and IV for each data file and then use the decrypted keys decrypt the larger data file.

Building the Module

The first two functions we need are one to create a certificate and one to check and see if the one we are creating already exists. These are easy since we can just use self signed certificates. Self Signed certs are fine for this use since we aren’t asking browsers or other systems to trust them. They are purely for our own use.


Import-Module PKI

<# 	.SYNOPSIS 		Get an existing clientss certificate from the certificate store. 	.DESCRIPTION 		Get an existing clients certificate from the LocalMachine\AddressBook certificate store and return it as an X509Certificate2 .NET object. 	.PARAMETER  Client 		The client name you would like to find. 	.EXAMPLE 		Get-ClientCert -client Testclient 	    Directory: Microsoft.PowerShell.Security\Certificate::localMachine\AddressBook 	Thumbprint                                Subject 	----------                                ------- 	EAE61338A4F802A989406506DC471A0C3A83F371  CN=Testclient 	 	.EXAMPLE 		Get-ClientCert -client "testClient2" | ForEach-Object { [IO.File]::WriteAllBytes("$($PWD.Path)\$($_.Subject).cer",($_.export('Cert', 'password'))); Get-Item "$($PWD.Path)\$($_.Subject).cer"} 		    Directory: C:\ 		Mode                LastWriteTime     Length Name 		----                -------------     ------ ---- 		-a---         3/26/2016   5:37 PM       2601 CN=testClient2.cer 		This example shows you how to get a client certificate and export its public key as a certificate file. This allows you to transport the public key to a client server for use encrypting files. 	.EXAMPLE 		"testClient2", "testClient3", "testClient4" | Get-ClientCert | ForEach-Object { [IO.File]::WriteAllBytes("$($PWD.Path)\$($_.Subject).p12",($_.export('PKCS12', 'password'))); Get-Item "$($PWD.Path)\$($_.Subject).p12"} 		    Directory: C:\ 		Mode                LastWriteTime     Length Name 		----                -------------     ------ ---- 		-a---         3/26/2016   5:37 PM       2601 CN=testClient2.p12 		-a---         3/26/2016   5:37 PM       2593 CN=testClient3.p12 		-a---         3/26/2016   5:37 PM       2601 CN=testClient4.p12 		This example shows you how to get a set of client certificates and export their full private and public keys. Most useful for importing into a new key store for server migrations. 		This is only necessary until the Export-ClientCert function is complete. 	.INPUTS 		System.String 	.OUTPUTS 		System.Security.Cryptography.X509Certificates.X509Certificate2 #>

function Get-ClientCert
{
	[outPutType([System.Security.Cryptography.X509Certificates.X509Certificate2])]
	param
	(
		[parameter(Mandatory = $true, ValueFromPipeline = $true)]
		[string]$client
	)

	process
	{
		Write-Output (Get-ChildItem cert:\localMachine\AddressBook | Where-Object Subject -EQ "CN=$client")
	}
}

<# 	.SYNOPSIS 		Create a new encryption certificate for a client. 	.DESCRIPTION 		Create a new self signed X509 certificate for a named client and output the public key to a file. 	.PARAMETER  client 		Client Name. 	.PARAMETER  outFolder 		Folder to out put Public Key File. 	.EXAMPLE 		PS C:\> New-ClientCert -client NewClient -outFolder c:\ClientPublicKeys

				Directory: C:\ClientPublicKeys

		Mode                LastWriteTime     Length Name
		----                -------------     ------ ----
		-a---         3/26/2016   4:05 PM        802 NewClient.cer

		This example shows how to call the New-ClientCert function with a single client name.

	.EXAMPLE
		PS C:\>$clients = "newClient4", "newClient5", "newClient6"
		PS C:\>$clients | New-ClientCert -outFolder c:\ClientPublicKeys
				 Directory: C:\ClientPublicKeys

		Mode                LastWriteTime     Length Name
		----                -------------     ------ ----
		-a---         3/26/2016   4:09 PM        799 newClient4.cer
		-a---         3/26/2016   4:09 PM        799 newClient5.cer
		-a---         3/26/2016   4:09 PM        799 newClient6.cer

		This example shows how to call the New-ClientCert function with multiple client names via pipeline.

	.INPUTS
		System.String,System.Int32

	.OUTPUTS
		System.io.FileInfo
#>

function New-ClientCert {
	[OutputType([System.IO.FileInfo])]
	param(
		[Parameter(Position=0, Mandatory=$true,ValueFromPipeline=$true)]
		[System.String]
		$client,
		[Parameter(Position=1)]
		[System.String]
		$outFolder
	)

	begin {
		if ($outFolder.IndexOf('.') -gt 0)
		{
			throw "-outFolder Parameter should be a folder, not a file name."
		}
	}
	process {

		if (Get-ClientCert -client $client)
		{
			Write-Warning "A certificate already exists for $client"
		}
		else
		{
			$cert = New-SelfSignedCertificate -DnsName $client -CertStoreLocation Cert:\LocalMachine\My | Move-Item -Destination Cert:\LocalMachine\AddressBook -PassThru

			if ($outFolder)
			{
				$outPath = Join-Path -Path $outFolder -ChildPath "$client.cer"
			}
			else
			{
				$outPath = Join-Path -Path $PWD.Path -ChildPath "$client.cer"
			}

			Export-Certificate -Cert $cert -FilePath $outPath -Type cer -NoClobber
		}
	}
}

These are pretty thin wrappers over existing commandlets, but they serve the purpose at hand. For instance, in this case I know that if I’m generating a self signed cert, I’m always going to want to export the public key for use on a remote server. So why write the commands to do the export myself each time I generate a cert? Wrap it in a function and viola, the cert is conveniently exported each and every time.

Also, notice in the function New-ClientCert that each cert we create gets immediately moved to the Address Book cert store. This is because we aren’t going to be asking the system to trust these certs in any way. I don’t need a browser to accept connections encrypted with these certs. I just need a place to keep named certificates.

To actually encrypt a file we use the following function.


<# 	.SYNOPSIS 		Encrypt a file using the specified named certificate 	.DESCRIPTION 		Use the public key from certificate named by the -client parameter to encrypt the data in the file specified by the -path paremeter. 		The data is encrypted and copied to a file in the same folder as the source file with .encrypted appended to the file name. 	.PARAMETER  client 		Find a certificate in the LocalMachine\AddressBook cert store with the Subject set to "CN=$client". 	.PARAMETER  path 		Path to file to be encrypted 	.EXAMPLE 		PS C:\>ConvertTo-EncryptedFile -path "c:\data.txt" -client "NewClient"

				 Directory: C:\

		Mode                LastWriteTime     Length Name
		----                -------------     ------ ----
		-a---         3/26/2016   4:09 PM        799 data.txt.encrypted

		This example shows how to call the ConvertTo-EncryptedFile with a single client and file.

	.EXAMPLE
		Get-Something 'One value' 32

	.INPUTS
		System.String,System.Int32

	.OUTPUTS
		System.IO.FileInfo

#>

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 = [bitconverter]::GetBytes($keyEncrypted.Length)
		[byte[]]$lenIV = [bitconverter]::GetBytes($aesManaged.IV.Length)

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

		$outFS.Write($lenK, 0, 4)
		$outFS.Write($lenIV, 0, 4)

		$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"
	}

}

This function is where we see the code from the linked MSDN article translated into Powershell. Going over how it functions in too much detail could take a while, but it’s basically outlined by the work flow detailed above.

That’s it for this post as I’ve run out of time, but in later posts we are going to fill out the support functions we need to make a usable module, and of course, we’ll work on getting the encrypted data back again into plain text or usable data.

Advertisements