Overview
This tutorial will show you how to encrypt files using the .NET Crypto API. We start off simple and add features one step at a time. All of the code is available for each step, so you can follow along or just play with the final version if you prefer.
Tutorial 1
In this tutorial we start with an empty project and add just enough code to encrypt a single file. The result is a console program that, given a file name, will encrypt that file and write it out to a new file. A sample of usage might look something like this:
tutorial01 myfile.txt myfile.crypto
Symmetric Encryption
Before we jump into the code, let's talk a little bit about the nature of cryptography. If you want to securely encrypt anything, you don't want to create your own encryption algorithms. There are lots of ways to encrypt data and most of them are wrong. Stick to proven methods that have been rigorously tested. Fortunately, we have several such algorithms available in .NET right out of the box, so we don't have to look to far.
When we encrypt lots of data (such as a file) we typically use a symmetric algorithm. In cryptography when we use the term symmetric we mean that the same credentials that are used to encrypt the data are used to decrypt the data. A common example of this is when you password protect a zip archive. You use a password when you lock the archive and you must use the same password to unlock the archive. If you lose or forget the password, you can't access the archive any more.
The current standard symmetric algorithm is called AES (Advanced Encryption Standard). It is heavily tested and relied upon by our government. It is definitely good enough for us and it is part of the .NET Crypto API.
A Basic File Copier
Start off by creating a new console project. Before we get into the details of encryption, let's get some basic functionality working. We simply want to copy the input file to the output file without making any changes to it. This way, we know we have something working that we can build on.
using System;
using System.IO;
using System.Security.Cryptography;
namespace Tutorial01
{
class Program
{
static void Main(string[] args)
{
if (args.Length != 2)
{
Console.WriteLine("You must provide the name of a file to read and the name of a file to write.");
return;
}
var sourceFilename = args[0];
var destinationFilename = args[1];
using (var sourceStream = File.OpenRead(sourceFilename))
using (var destinationStream = File.Create(destinationFilename))
{
sourceStream.CopyTo(destinationStream);
}
}
}
}
In the above code, that we require two arguments be provided. If not, we return with a friendly tip. Otherwise, we get the source file name as the first argument and the destination file name as the second argument. You might want to add some code to make sure the source file exists, but I'm going to skip that so we can focus on the core mechanics. If you are not familiar with the using
statement, you may want to look that up. In C# that is a way to cleanup objects when we are done with them. It is particularly useful for file operations.
The real work is the five lines of code that start on line 19 with the using
statement. We open our source file for reading (line 19) and we create our destination file for writing (line 20). We get a stream for each file and simply copy the contents of one file to the other using the CopyTo
method (line 22). If you are following along, this is a good time to make sure you can get this working and that you can create the target file identical to the source.
Encryption At Last
Since we are already copying files using Stream
objects, we use the CryptoStream
API to add encryption to the existing operation.
using System;
using System.IO;
using System.Security.Cryptography;
namespace Tutorial01
{
class Program
{
static void Main(string[] args)
{
if (args.Length != 2)
{
Console.WriteLine("You must provide the name of a file to read and the name of a file to write.");
return;
}
var sourceFilename = args[0];
var destinationFilename = args[1];
using (var sourceStream = File.OpenRead(sourceFilename))
using (var destinationStream = File.Create(destinationFilename))
using (var provider = new AesCryptoServiceProvider())
using (var cryptoTransform = provider.CreateEncryptor())
using (var cryptoStream = new CryptoStream(destinationStream, cryptoTransform, CryptoStreamMode.Write))
{
sourceStream.CopyTo(cryptoStream);
}
}
}
}
The only thing we did here was add a few lines of code (lines 22, 23, 24) and made a slight alteration to the stream copy (line 26). Line 22 creates a new AesCryptoServiceProvider
. This provider has information about how we want to encrypt. We will look at that more later. For now, we just want to use it to create the decryptor transform that we need (line 23). If we wanted to do things the hard way we could use the transform directly, but it's easier to use the CryptoStream
. On line 24 we create the CryptoStream
. We need to give it the stream to write to, the transform (in our case the encryptor) and let it know that we want this to be a writable stream. The CryptoStream
wraps our destination stream. That is, it takes the bytes that need to be written and processes (encrypts) the bytes and then sends the processed bytes to the destination stream. We basically inject our encryption between the two file streams that we are already using. Line 26 shows how we achieve this by calling CopyTo
on the sourceStream and providing our new CryptoStream
.
When you run this program, your output file will be securely encrypted. You should test it to make sure. Your output file should like like a bunch of seemingly random bytes.
Wait a Minute!
If everything went well, you have an encrypted file. But remember, above, we mentioned symmetric encryption? Symmetric encryption uses the same credentials to decrypt and encrypt the data. That suggests that we need to have some kind of credentials in the first place. AES uses a key and supports different key sizes. By default the key length is 256 bits (32 bytes). A key is simply any 32 bytes of data we want to use. Whatever key we use to encrypt the data will be needed to decrypt the data. If we decrypt the data with the wrong key we will get garbage. But what key did we use above? We don't seem to specify one at all.
When an AesCryptoServiceProvider
is created, it automatically creates a secure random key. We are just using whatever it randomly created. That is fine, but if we are ever going to decrypt the file we will need to know what the key is. Let's add some code to display the AES encryption key that was used.
{
sourceStream.CopyTo(cryptoStream);
Console.WriteLine(System.Convert.ToBase64String(provider.Key));
}
Not so tough. We just get the Key
property of the AesCryptoServiceProvider
. The key is an array of bytes, which won't print very nicely. Using System.Convert.ToBase64String
makes it so we can represent all of those byte values as printable characters. We could have converted the byte array to a hex string or any number of things, but Base64 is convenient.
Oh, And One More Thing...
When using AES we have the key, which is secret and should be protected. But there is another piece of data that is used to make the encryption stronger. It is called the IV (initialization vector). The IV tells the encryptor how to modify the first block to prevent against certain attacks. Changing the IV will change the encryption/decryption results, so we are going to need to have that available when we decrypt the file. The IV is not secret, however. So we don't need to hide it. Therefore, we can write it directly to the output file we are already writing. That way we can't possibly lose it. We do have to be careful though. If we write the IV to the file and the bytes get encrypted then we won't be able to get them back out (since we need them to do the decryption). We have to make sure to write the IV before any encryption happens. It only takes one line of code:
destinationStream.Write(provider.IV, 0, provider.IV.Length);
But, where we put that code is very important:
{
destinationStream.Write(provider.IV, 0, provider.IV.Length);
sourceStream.CopyTo(cryptoStream);
Console.WriteLine(System.Convert.ToBase64String(provider.Key));
}
We can write directly to the destination stream (not the CryptoStream
) before the ecryption that happens in the call to CopyTo
. That way, the IV is not encrypted, but is right at the beginning of the destination file.
Now we are looking pretty good. We encrypt a file. We save the IV in the file where it is easy to find and we display the key, so you save it in a secret place until it is needed to decrypt the file.
Tutorial 2
In this tutorial we decrypt an ecrypted file, restoring it to its original state. To do this, we need three things: the key used to encrypt the file, the IV used to encrypt the file, and the encrypted file. From our last tutorial, we display the key in the console after encrypting the file and we store the IV at the front of the file. So we have averything we need.
Getting Ready
Our current program accepts two filenames as command line arguments. Let's modify it to accept a third argument, the decryption key. Because it helps us enderstand how the program works, let's go ahead and update the help text in the program.
if (args.Length < 2 || args.Length > 3)
{
Console.WriteLine("Encrypt or Decrypt a file.");
Console.WriteLine("\nTutorial02 source destination [key]\n");
Console.WriteLine("{0,-15}Specifies the source file.", "source");
Console.WriteLine("{0,-15}Specifies the destination file.", "destination");
Console.WriteLine("{0,-15}The optional decryption key.", "key");
Console.WriteLine("{0,-15}If provided we decrypt, otherwise we encrypt.", "");
return;
}
Notice that we now allow for two or three arguments. If the third argument is provided, we use it as the key for decrypting the source file. Now we are ready to get the values from the coomand line.
var sourceFilename = args[0];
var destinationFilename = args[1];
byte[] key = null;
if (args.Length == 3)
{
key = System.Convert.FromBase64String(args[2]);
}
Remember that we wrote out the key using Base64 encoding, so when we get the key we convert it back from Base64 to the byte array that we need. For the rest of the code, we now know that if key
is not null
, we want to decrypt the source file. If key
is not null
we want to encrypt the source file. For the existing encryption code we just wrap it inside an if
statement.
if (key == null)
{
// Encrypt the source file and write it to the destination file.
using (var sourceStream = File.OpenRead(sourceFilename))
using (var destinationStream = File.Create(destinationFilename))
using (var provider = new AesCryptoServiceProvider())
using (var cryptoTransform = provider.CreateEncryptor())
using (var cryptoStream = new CryptoStream(destinationStream, cryptoTransform, CryptoStreamMode.Write))
{
destinationStream.Write(provider.IV, 0, provider.IV.Length);
sourceStream.CopyTo(cryptoStream);
Console.WriteLine(System.Convert.ToBase64String(provider.Key));
}
}
Nothing changed from our existing encryption code; we just put it all in the if
block.
Decryption
We can finally get to the decryption code. It looks a like like the encryption code and lives inside the else block of the previous if
statement.
else
{
// Decrypt the source file and write it to the destination file.
using (var sourceStream = File.OpenRead(sourceFilename))
using (var destinationStream = File.Create(destinationFilename))
using (var provider = new AesCryptoServiceProvider())
{
var IV = new byte[provider.IV.Length];
sourceStream.Read(IV, 0, IV.Length);
using (var cryptoTransform = provider.CreateDecryptor(key, IV))
using (var cryptoStream = new CryptoStream(sourceStream, cryptoTransform, CryptoStreamMode.Read))
{
cryptoStream.CopyTo(destinationStream);
}
}
}
The structure of this new code is very similar the the encryption code. Let's look at the differences. We need to read the IV from the file before we can decrypt it. We actually have to get it before we can create the decryption transform, since the transform needs the key and the IV to do it's work. So, we break up our using
statements a little bit so we can put our logic in where it needs to be. We pass the key and the IV to CreateDecryptor
and we are ready to decrypt. We just call CopyTo
on the CryptoStream
and it will decrypt the bytes and write them to the destination.
Summary
Now we can encrypt files and get the secret key and we can use that secret key to restore the file. This is the basic start of a file encryption system. If you want to encrypt a few files, this will work well because you only need to keep a few keys. If you want to encrypt a lot of files, this will get out of control as the number of keys grows. That is a topic for a future post, but in the mean time you could zip a bunch of files and then encrypt the single zip file if you needed to.
Last updated: April 5, 2015