Simple crypto in C#

This article was originally published in VSJ, which is now part of Developer Fusion.
The trouble with cryptography is that you often need to use it, but most of us aren’t experts. Suppose you have a list of web pages and passwords, or credit cards and pin numbers, and you need to save them to disk for later use. Of course they have to be protected, but what do you know about cryptography?

Fortunately the .NET framework contains classes that make cryptography fairly easy, and with just a few simple ideas you can implement your own cryptographic solutions without needing to invest the necessary time to become an expert.

Hash the password

It’s a common error to think that to use password protection you need to store the password on the computer. The algorithm goes: get the password, compare it to a list of stored user names and passwords, and allow or deny access depending on whether it is or isn’t in the list. This isn’t a good idea because the password list is fairly easy to get at. If at all possible you should always avoid storing passwords on disk unless they are encrypted, and this raises the problem of what to do with the password that unlocks the stored password list. A much better method is to use the password to generate a hash value and store this on disk. The point being that a hash value can easily be reconstructed from the password but the password is very difficult to construct from the hash value. The algorithm now is that the user presents the password, then a hash value is generated from it and compared to the stored hash value.

A slightly different approach is to use the password supplied to generate a hash value and use this as an encryption key, and then see if it is possible to read a file that was created using this key. Storing the hash value or the password itself at the start of the file makes it possible to check that the file has indeed been decrypted correctly. If so, the user has supplied the correct password. In this scheme the password is the encryption/decryption key, and while this isn’t as strong as using a random large encryption key, it’s good enough for most purposes.

For example, we can create a class – CProfile – that will accept the password and turn it into a key suitable for use with an encryption method. We need some “using” statements and some global variables that we will use later:

using System.IO.IsolatedStorage;
using System.IO;
using System.Security.Cryptography;
using System.Windows.Forms;

namespace PassKey
{
	class CProfile
	{
		private byte[] key;
		private
	RC2CryptoServiceProvider rc2;
		private
	IsolatedStorageFile IsoStore;
		private
IsolatedStorageFileStream ISoStream;
		private CryptoStream cs;
		private StreamReader sr;
		private StreamWriter sw;
		private string recPass;
		private string m_password;
The constructor creates an instance of CProfile and generates an encryption key which is stored for later use in the global variable “key”:
public CProfile(string password)
{
	PasswordDeriveBytes PassBytes =
		new PasswordDeriveBytes(
		password, null);
	key = PassBytes.CryptDeriveKey(
		“RC2”, “SHA1”, 128, new byte[]
		{ 0, 0, 0, 0, 0, 0, 0, 0 });
The 128-bit key is created from the password using the SHA1 hash method so as to be suitable for the RC2 encryption method. Knowledge of how these algorithms work would make you a cryptography expert – you can take it on trust that they are good methods that provide strong encryption as long as the password is strong.

Now that we have a key, we can create the crypto object that implements the RC2 algorithm:

rc2 = new RC2CryptoServiceProvider();
rc2.Key = key;
rc2.IV = new byte[] { 21, 22, 23, 24,
	25, 26, 27, 28 };
The IV or initial value property should be set to a random value, but the same random value each time. In this case setting it to a set of fixed values is as secure.

If the encrypted file already exists then we next attempt to read it. We need something at the start which can be used to detect a correct decryption, i.e. that the password is correct. You might think that the thing to do is to set the file up with a fixed word that can be encoded and decoded but this would make the hacker’s job easier as every file would have the same initial plain text. The simplest thing to do is to store the encrypted password itself and then try to recover it using the password that the user supplied.

Isolated storage

The next question is where to store the file? There is always a problem in setting up files to store custom application data without running into problems with files/directories of the same name already existing etc. The .NET system provides a solution to this too with its “isolated storage” facility. You can use this to work with an area of storage that is unique to the current user and the application.
IsoStore =
	IsolatedStorageFile.GetStore(
	IsolatedStorageScope.User |
	IsolatedStorageScope.Domain |
	IsolatedStorageScope.Assembly,
	null, null);
Now we have an IsoStore object that is unique to the user, domain and assembly, i.e. the user and the program. This will be a folder created in Documents and Settings under the user’s name – you can discover exactly where the file is by examining the IsoStore object in the debug window. Notice that to create or access an isolated storage file the user has to have successfully logged on to the machine that it is stored on – yet another layer of protection. Finally it’s worth knowing that isolated storage “roams” with the user, and hence is a simple way of centralising the encrypted data. Note that this only works if a roaming profile is set up.

If the file doesn’t exist then we have to create it using the new password that the user has just supplied. You could give the file a descriptive name like “secret data.dat”, but why make things easy for the hackers?

private const string
profile = @”\IDDQ756.ATX”;
The name is constructed to look confusing and not give much away about what it’s for. Next we check to see if it exists and create it if it doesn’t:
if (IsoStore.GetFileNames(profile)
	.Length == 0)
{
	sw= OpenWrite();
	Close();
}
The OpenWrite function is a custom method that returns a StreamWriter object which can be used to write text to the encrypted file. Before returning the function also adds the encrypted password to the start of the file.

Now that the file exists, either because we have just created it using the new password or created it some time ago, we can open it, read the stored password into recPass and compare it to the supplied password:

sr=OpenRead();
Close();
if (recPass != m_password)
	Application.Exit();
}
Notice that the OpenRead function is implemented to automatically read the password as part of opening the file. If the passwords don’t match we just stop the application – you could add something that tells the user what is happening. The OpenRead function also returns a StreamReader object which allows the encrypted file to be read as text.

Cryptostreams

The problem with the above implementation is that we haven’t actually solved the problem of opening the encrypted streams. Many of the cryptographic examples that you will find don’t make good use of streams. The key idea is that, as long as it makes sense, you should be able to connect streams together. For example in the OpenWrite function we can first create an IsolatedStorageFileStream:
public StreamWriter OpenWrite()
{
	ISoStream =
	new IsolatedStorageFileStream(
		profile,
		FileMode.Create,
		FileAccess.Write,
		IsoStore);
This is a byte stream which can be used to write raw bytes to a file in the isolated storage just created. We can “feed” this stream with encrypted bytes by creating a “connected” CrytpoStream:
cs = new CryptoStream(
	ISoStream,
	rc2.CreateEncryptor(),
	CryptoStreamMode.Write);
This too is a byte stream. It accepts bytes, encrypts them using the RC2 method created earlier, and passes them along to the ISoStream to be written to the file. We could stop at this point, but we really want to write strings to the crypto stream so why not create one more stream, a StreamWriter connected to the CryptoStream:
sw = new StreamWriter(cs);
All that remains is to write the password to the file and return the StreamWriter:
sw.WriteLine(m_password);
return sw;
Now when we send strings to the StreamWriter it converts them to bytes which the CryptoStream encodes and finally the IsoStream writes to the file. Using streams in this way is what they were invented for, and why they are a better way of working than just reading and writing files.

The OpenRead function is another example of concatenated streams:

public StreamReader OpenRead()
{
ISoStream =
	new IsolatedStorageFileStream(profile,
	FileMode.Open,
	FileAccess.Read,
	IsoStore);
cs = new CryptoStream(
	ISoStream,
	rc2.CreateDecryptor(),
	CryptoStreamMode.Read);
sr=new StreamReader(cs);
	recPass = sr.ReadLine();
	return sr;
}
It plugs the same three sorts of streams together to get an isolated storage stream feeding a cryptostream, feeding a “string” stream.

The Close function has to close each of the streams:

public void Close()
{
	if (sw != null)
	{
		sw.Close();
		sw=null;
	}
	if (sr != null)
	{
		sr.Close();
		sr=null;
	}
	if (cs != null)
	{
		cs.Close();
		cs=null;
	};
	if (ISoStream != null)
	{
		ISoStream.Close();
		ISoStream = null;
	}
}

CProfile in use

Start a new Windows Form project and add a new form called PassWordDlg. Set the new form’s properties to: FormBorderStyle to FixedDialog, ControlBox, MinimizeBox and MaximizeBox – all set to false. Next add two buttons and a textbox. Call one button OK and the other Cancel and the textbox PassWordBox. If you want to hide the entry of the password you can set the textbox’s PasswordChar property to an asterisk (or any other character) in order to obscure the entry.

The .NET system has additional facilities that make implementing dialog boxes very easy. Each button can be associated with a DialogResult property which essentially automates the OK/CANCEL handling. Set the OK button’s DialogResult property to OK and the Cancel button’s to Cancel. To allow the program that uses the dialog box to gain access to any data the user has entered you could define a struct that stores all of the data in the user interface. In this case, however, as there is only a single text box a simple string property will do the job just as well. Within the class definition of PassWordDlg add:

public string PassWord
{
	get
	{
		return PassWordBox.Text;
	}
}
We don’t need a set method for the property for obvious reasons.

Now the main form can make use of the password box by simply creating an instance and using the DialogResult property and object:

private void Form1_Load(
	object sender, EventArgs e)
{
	PassWordDlg PW=new PassWordDlg();
	PW.ShowDialog();
	if (PW.DialogResult ==
		DialogResult.Cancel)
		Application.Exit();
	password = PW.PassWord;
	PW.Dispose();
}
We also need a global variable to store the password:
public partial class Form1 : Form
{
	private string password;
Notice that if the user clicks Cancel then the whole application comes to an end. Otherwise the password is retrieved for later use.

We can use a dataGridView control to store a table of data that isn’t bound to a database, which is a technique worth knowing about. Add a dataGridView control to a form and add columns for Site, URL, user name and password, or for whatever data you want to store securely. Also add a button with the title “Add/Edit” (see Figure 1).

Figure 1
Figure 1: Using the editor to customise the dataGridView

Now we can add to the end of the FormLoad event handler:

	profile = new CProfile(password);
	ReadData();
}
We also need a global variable to keep track of the profile object:
private CProfile profile;
The idea is that the user clicks the “Add/Edit” button, changes or adds data to the grid, and when they click the button again it is written to the encrypted file:
private void button1_Click(
	object sender,
	EventArgs e)
{
	if (editing)
	{
		editing = false;
		button1.Text = “Add/Edit”;
		WriteData();
	}
	else
	{
		editing = true;
		button1.Text = “Finish”;
	}
}
We also need a global variable to keep track of the mode, i.e. whether you are adding or editing:
private bool editing = false;
The WriteData function is fairly easy. It simply opens the file and writes out the current contents of the grid in row order:
private void WriteData()
{
	StreamWriter sw =
		profile.OpenWrite();
	int rows = dataGridView1.
		Rows.Count - 2;
	sw.WriteLine(rows.ToString());
	int cols = dataGridView1.
		Columns.Count - 1;
	sw.WriteLine(cols.ToString());
	for(int i=0;i<= rows;i++)
	{
		for(int j=0;j<=cols;j++)
		{
			sw.WriteLine(dataGridView1.
				Rows[i].Cells[j].Value);
		}
	}
	profile.Close();
}
Notice that the first two values stored to the file are the number of rows and columns to be stored. This is used by the ReadData function to discover how much data to read:
private void ReadData()
{
	StreamReader sr =
		profile.OpenRead();
	if (sr.EndOfStream)
	{
		profile.Close();
		return;
	}
	int rows = Convert.ToInt32(
		sr.ReadLine());
	int cols = Convert.ToInt32(
		sr.ReadLine());
	for (int i = 0; i <= rows; i++)
	{
		dataGridView1.Rows.Add();
		for (int j = 0; j <= cols; j++)
		{
			dataGridView1.Rows[i].
				Cells[j].Value =
				sr.ReadLine();
		}
	}
	profile.Close();
}
If you run the entire project you should find that it all works as described. The CProfile class is generally useful if you want to store encrypted data in isolated storage.

In practice there are a few potential problems with isolated storage that you need to be aware of. Each time the application changes so does the folder used for isolated storage, and this makes it inaccessible with the potential loss of data. To make sure that data isn’t lost you need to give your application a strong name that doesn’t change.


Ian Elliot is a development programmer with I/O Workshops Ltd, a consultancy which solves real world problems for clients with widely varying requirements.

You might also like...

Comments

About the author

Ian Elliot United Kingdom

Ian Elliot is a development programmer with I/O Workshops Ltd, a consultancy which solves real-world problems for clients with widely varying requirements.

Interested in writing for us? Find out more.

Contribute

Why not write for us? Or you could submit an event or a user group in your area. Alternatively just tell us what you think!

Our tools

We've got automatic conversion tools to convert C# to VB.NET, VB.NET to C#. Also you can compress javascript and compress css and generate sql connection strings.

“Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.” - Brian Kernighan