Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Cryptor - Encrypt Files With Rijndael 256 bit

0.00/5 (No votes)
20 Jan 2006 1  
A simple utility for encrypting files using 256 bit Rijndael (AES). Also, adds menu items to Windows Explorer's file context menu for easy access.

Cryptor - Encryption Interface

Shell context menu with Cryptor installed

Contents

Introduction

Most encryption tools are big, bulky things that store your password and act like safes - things that I didn't want. Also, I enjoy coding, so my natural inclination was to write my own . This is my attempt to create a simple app that just works on single files with a single password and then closes. Also, to add some form of shell context menu interaction so that encrypting files are even simpler.

Oh, and by the way, this is my first article - so play nice .

The App

Cryptor uses the 256 bit Rijndael (otherwise known as AES, or the Advanced Encryption Standard) encryption to secure files. It does this using a CryptoStream object and the RijndaelManaged class. There is no default form to the application - double clicking Cryptor.exe will achieve nothing. A form is only displayed when the correct command line arguments are passed. This happens when a user has installed the MSI (or added the neccessary keys to the registry) and right clicked on a file (not a folder) in Windows Explorer.

The files used in the application are explained below:

  • frmSimpleFile - This is the form used when a user right clicks on a file in Windows Explorer.
  • EncryptionRoutines - This is the main encryption engine. It performs all of the initialisation and file transformation functions.
  • AssemblyInfo - The standard assembly information file.

All of the encryption routines happen in the aptly named EncryptionRoutines class. The various features of this class are discussed below.

Initialising The Encryption Engine

The Initialise routine creates the instance of the RijndaelManaged class, rijM, used throughout the rest of the EncryptionRoutines class - hence it must be called before either the TransformFile or the CancelTransform method is called. It starts by hashing the given password (sPWH) using the MD5 hash algorithm. This is then used as the salt for the PasswordDeriveBytes class. The GetBytes method of the PasswordDeriveBytes class is then used to extract 32 bytes (256 bits) for the key and 16 bytes (128 bits) for the IV. The key and IV are then assigned to rijM. Also, the padding mode of rijM is set to PKCS7 (the default value, but I thought I would explicitly declare it to make it clearer). This is, in my opinion, by far the best choice of padding. See notes on padding for more info.

Public Sub Initialise(ByVal sPWH As String)
    'initialise rijM

    rijM = New RijndaelManaged
    'derive the key and IV using the

    'PasswordDeriveBytes class

    Dim pdb As PasswordDeriveBytes = _
        New PasswordDeriveBytes(sPWH, _
          New MD5CryptoServiceProvider().ComputeHash(ConvertStringToBytes(sPWH)))
    'extract the key and IV

    bKey = pdb.GetBytes(32)
    bIV = pdb.GetBytes(16)

    'initialise headerBytes

    headerBytes = ConvertStringToBytes(headerString)

    With rijM
        .Key = bKey '256 bit key

        .IV = bIV '128 bit IV

        .BlockSize = 128 '128 bit BlockSize

        .Padding = PaddingMode.PKCS7
    End With

    bInitialised = True
End Sub

The Main Encryption Routine

The encryption process takes place in the function TransformFile in the EncryptionRoutines class. In order to tell if the password entered is correct on decryption, Cryptor adds a short string ("CRYPTOR") as a header to the plaintext (before encryption) so that when decrypting the file, it can check to see if the first 7 bytes correspond to "CRYPTOR". If they do, then it will continue with the decryption. If they don't then it will cancel the decryption, delete the output file (which contains nothing, but does exist) to tidy up, and then the function raises the event Finished(ByVal retVal As ReturnType) with retVal = ReturnType.IncorrectPassword. The addition of this header string does not in any way compromise the strength of the encryption, as the Rijndael algorithm is designed with this in mind. Also, the block size used in the encryption is 16 bytes (128 bits) and this header takes up only 7, so even if someone knew the header was "CRYPTOR", they still wouldn't be able to backward engineer it to obtain the key or IV.

The following routine uses a 4 KB buffer when reading in the input file, and requires that the Initialise method has already been called.

Public Function TransformFile(ByVal sInFile As String, _
       ByVal sOutFile As String, _
       Optional ByVal encrypt As Boolean = True) As Boolean
    'make sure that all the initialisation has been completed:

    If Not bInitialised Then
        RaiseEvent Finished(ReturnType.Badly) : Return False
    If Not IO.File.Exists(sInFile) Then
        RaiseEvent Finished(ReturnType.Badly) : Return False

    Dim fsIn As FileStream = Nothing
    Dim fsOut As FileStream = Nothing
    Dim encStream As CryptoStream = Nothing
    Dim retVal As ReturnType = ReturnType.Badly
    Try
        'create the input and output streams:

        fsIn = New FileStream(sInFile, FileMode.Open, _
                   FileAccess.Read)
        fsOut = New FileStream(sOutFile, _
                    FileMode.Create, FileAccess.Write)

        'some helper variables

        Dim bBuffer(4096) As Byte '4KB buffer

        Dim lBytesRead As Long = 0
        Dim lFileSize As Long = fsIn.Length
        Dim lBytesToWrite As Integer

        If encrypt Then
            encStream = New CryptoStream(fsOut, _
              rijM.CreateEncryptor(bKey, bIV), _
              CryptoStreamMode.Write)
            'write the header to the output file 

            'for use when decrypting it

            encStream.Write(headerBytes, 0, headerBytes.Length)
            'this is the main encryption routine. 

            'it loops over the input data in blocks of 4KB,

            'and writes the encrypted data to disk

            Do
                If bCancel Then Exit Try
                lBytesToWrite = fsIn.Read(bBuffer, 0, 4096)
                If lBytesToWrite = 0 Then Exit Do
                encStream.Write(bBuffer, 0, lBytesToWrite)
                lBytesRead += lBytesToWrite
                RaiseEvent Progress(CInt((lBytesRead / lFileSize) * 100))
            Loop
            RaiseEvent Progress(100)
            retVal = ReturnType.Well
        Else
            encStream = New CryptoStream(fsIn, _
              rijM.CreateDecryptor(bKey, bIV), _
              CryptoStreamMode.Read)

            'read in the header

            Dim test(headerBytes.Length) As Byte
            encStream.Read(test, 0, headerBytes.Length)

            'check to see if the file header reads correctly.

            'if it doesn't, then close the stream & jump out

            If ConvertBytesToString(test) <> headerString Then
                encStream.Clear()
                encStream = Nothing
                retVal = ReturnType.IncorrectPassword
                Exit Try
            End If

            'this is the main decryption routine. 

            'it loops over the input data in blocks of 4KB,

            'and writes the decrypted data to disk

            Do
                If bCancel Then
                    'if the cancel flag is set,

                    'then jump out

                    encStream.Clear()
                    encStream = Nothing
                    Exit Try
                End If
                lBytesToWrite = encStream.Read(bBuffer, 0, 4096)
                If lBytesToWrite = 0 Then Exit Do
                fsOut.Write(bBuffer, 0, lBytesToWrite)
                lBytesRead += lBytesToWrite
                RaiseEvent Progress(CInt((lBytesRead / _
                                    lFileSize) * 100))
            Loop
            RaiseEvent Progress(100)
            retVal = ReturnType.Well
        End If
    Catch ex As Exception
        Console.WriteLine("*****************ERROR*****************")
        Console.WriteLine(ex.ToString)
        Console.WriteLine("****************/ERROR*****************")
    Finally
        'close all I/O streams (encStream first)

        If Not encStream Is Nothing Then
            encStream.Close()
        End If
        If Not fsOut Is Nothing Then
            fsOut.Close()
        End If
        If Not fsIn Is Nothing Then
            fsIn.Close()
        End If
    End Try
    'only delete the file if the password was bad, and

    'therefore its only an empty file

    If retVal = ReturnType.IncorrectPassword Then
        IO.File.Delete(sOutFile)
    End If
    'raise the Finished event, and then reset bCancel

    RaiseEvent Finished(retVal)
    bCancel = False
End Function

Notes On Padding

The Rijndael algorithm is a symmetric block cipher. This means that it encrypts input data in small blocks at a time. The size of each block is a variable which can be defined by editing the BlockSize property of an instance of the RijndaelManaged class. Cryptor sets this value to 128 bits (so that it complies with the AES). The fact that it encrypts in blocks means that the input data needs to be an exact multiple of the block size. This isn't going to happen all the time - so padding needs to be used. There are three types of padding available in v1.1 of the .NET Framework:

  • PaddingMode.None - This isn't really a padding at all. It requires that the input file is an exact multiple of the block size. If this isn't the case, bad things will happen.
  • PaddingMode.Zeros - This simply adds null bytes to the end of the final block until it is full. This can be troublesome, as, if the final block before padding already has zero bytes on the end, these will be removed along with the padding. So when it is encrypted, you'll have to store the original file size along with the file so that you can add the necessary number of zero bytes back onto the file. Not brilliant I might add.
  • PaddingMode.PKCS7 - This type of padding is definitely the best. It works by counting the number of bytes required to fill the final block and then filling the block with each new byte set to this value. For example, if the block size required is 16 bytes and there are only 4 left in the input file (so there are 12 bytes of padding needed), the cipher will add "0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C" (that is 12 bytes, each of the value 0C, or 12) to the end of the input file. This means that the original file can be restored exactly without any cumbersome helper routines (like storing the original file size, etc...). One thing to note is that if the file is, by chance, an exact multiple of the block size required, an entirely new block will be added (this will be "10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10" in the case of a 16 byte block size). This ensures that the original file can be restored given any circumstances.

Rijndael And AES

When I say that Rijndael is AES, I'm actually lying. They are the same algorithm, however Rijndael has room for more block and key sizes. Rijndael supports key and block sizes of 128, 160, 192, 224, and 256 bits, whereas AES supports only one block size - 128 bits, and one of 128, 192, or 256 bit for the key size (256 bit is specified for use on documents classified as top secret - so it should be okay to use on your bank statements ).

Quote from Wikipedia:

AES is an encryption standard adopted by the US government. It was adopted by the National Institute of Standards and Technology (NIST) as US FIPS PUB 197 in November 2001 after a 5-year standardisation process.

Using the code

The encryption methods in the code are all contained in the EncryptionRoutines class. If you are looking to use encryption in your own apps, then this class contains all the necessary methods:

  • GenerateHash - Uses the SHA384Managed class to create a 384 bit (48 byte) hash string.
  • Initialise - Needs to be called before any transformation can take place.
  • TransformFile - Either encrypt or decrypt a file.
  • CancelTransform - Cancels the encryption process if it is running.
  • ConvertBytesToString - Converts an array of bytes to the corresponding Unicode string.
  • ConvertStringToBytes - Hmmm... Not sure what that does ;)

So, the easiest way to encrypt a file would be the following:

Public Sub EncryptFile(ByVal pass As String)
    Dim routines As New EncryptionRoutines()
    routines.Initialise(pass)
    routines.TransformFile(tbInputFile.Text, _
                     tbOutputFile.Text,True)
End Sub

Of course, you would probably need to improve on this code to enable threading and taking care of the Progress and Finished events exposed by the EncryptionRoutines class.

Adding An Item To The Shell's Context Menu

I wanted this application to be as easy to use as possible, so that even my mum could cope . I decided it would be nice to have two context menu items on the Windows Explorer file context menu, so that when the user right clicks on any file in Windows Explorer, they would be able to select "Encrypt File" and "Decrypt File", and it would transform the contents of the file and not create a new one, or change the old one's file name (a sort of pseudo-in-place transform).

There are two options when adding to the shell's context menu:

  • Static - This is the easiest way to add menu items, and requires only a few simple registry entries.
  • Dynamic - This requires a lot more effort. A dynamic context menu item requires you to create an ActiveX control that implements the IShellExtInit and IContextMenu interfaces.

Although dynamic menu items offer much more functionality, I didn't need any of it, and so I opted for the static route.

As an example, the following explains how to set up a static context menu item that will appear whenever a user right clicks any file, that will allow them to open the file in Notepad:

  1. Open regedit (Start -> Run => regedit).
  2. Select HKEY_CLASSES_ROOT\*\shell\ and create a new key (called something like NotepadEdit).
  3. Set the default value of this key to what you want to be shown on the context menu item (e.g. "Open with Notepad").
  4. Create a new key under this one called "Command" (e.g. HKEY_CLASSES_ROOT\*\shell\NotepadEdit\Command\).
  5. Set the default value of this key to "notepad.exe %1".
  6. Now, see if it works, by right clicking a file (not a folder) in Windows Explorer, and clicking "Open with Notepad" (if that's what you called it). The file you chose should open in Notepad.

To add "Encrypt File" and "Decrypt File" to the shell's context menu, I needed to add two sets of registry keys (one for encryption, the other for decryption).

I created these:

  • HKEY_CLASSES_ROOT\*\shell\CryptorEncrypt\ - for encryption
    • Set default value to "Encrypt File".
    • Create new key at HKEY_CLASSES_ROOT\*\shell\CryptorEncrypt\Command.
    • Set the new Command key's default value to <path to cryptor.exe> %1 1.
  • HKEY_CLASSES_ROOT\*\shell\CryptorDecrypt\ - for decryption
    • Set default value to "Decrypt File".
    • Create new key at HKEY_CLASSES_ROOT\*\shell\CryptorDecrypt\Command.
    • Set the new Command key's default value to <path to cryptor.exe> %1 0.

These keys are added during installation using the MSI file linked to at the top of the page, so that the user doesn't need to.

The 1 and 0 at the end of the Command key's default values indicate encryption and decryption respectively. An explanation of the necessary command line arguments can be found next...

Command Line Access

If you double click on Cryptor.exe, nothing will happen. It does not have a default form. It will only display a form if the command line arguments passed are of the format described below.

  • One argument - If this argument is a file, then load frmSimpleFile so that it is ready to encrypt.
  • Two arguments - If the first argument is a file and the second is an integer (either 0 or 1), then load frmSimpleFile ready to encrypt if the integer passed is 1, or to decrypt if it is 0 (or anything else for that matter).

The following code loads the correct form based on the arguments. It is from Module Main, found in the frmSimpleFile.vb file:

<STAThread()> _
Sub Main(ByVal args() As String)
    Application.EnableVisualStyles()
    Application.DoEvents()
    Select Case args.Length
        Case 1
            Dim frm As New frmSimpleFile(True, _
                       GetLongName(args(0)))
            If frm.isOK Then
                Application.Run(frm)
            End If
        Case 2
            If Not IsNumeric(args(1)) Then
                MessageBox.Show("Invalid input arguments: " & _
                   args(1) & " cannot be converted to an integer" & _
                   vbCrLf & vbCrLf & "Required Argument" & _ 
                   " format: Cryptor.exe [<file_to_use>" & _ 
                   " [<1 -> encrypting>]]", "Error", 
                   MessageBoxButtons.OK, MessageBoxIcon.Error)
                Return
            End If
            Dim frm As New frmSimpleFile(args(1) = 1, _
                           GetLongName(args(0)))
            If frm.isOK Then
                Application.Run(frm)
            End If
    End Select
End Sub

Converting Short To Long File Names

When the file name is passed to Cryptor through command line arguments, it is in its short form (DOS 8.3). So something that should be (long form):

C:\Documents and Settings\Will\My Documents\Visual Studio Projects\network image.png

would get to Cryptor in the (short) form:

C:\DOCUME~1\Will\MYDOCU~1\VISUAL~1\NETWOR~1.PNG

The only problem this causes on Win XP is that the title of frmSimpleFile would say Encrypt File 'NETWOR~1.PNG', rather than Encrypt File 'network image.png'. When the encrypted file is copied back over to the location the original was, the long file name is restored. So that isn't that much of a problem.

In Win 98 however, the file name is not restored to the long form, so encrypting a file name of which is longer than 8 characters will cause it to be changed after encryption/decryption. This is both annoying and much more of a problem than it is in Win XP. For this reason, I needed to come up with a way to convert the file name passed in the command line arguments into its long form before passing it to frmSimpleFile. This is done in Module Main.

The conversion uses the Win32 API GetLongPathName, found in kernel32.dll. The code used to convert the file name is shown below:

Private Declare Function GetLongPathName Lib "kernel32" _
    Alias "GetLongPathNameA" (ByVal lpszShortPath As String, _
    ByVal lpszLongPath As String, _
    ByVal cchBuffer As Integer) As Integer

Private Function GetLongName(ByVal sShortFileName _
                 As String) As String
    Dim lRetVal As Long, _
        sShortPathName As String, iLen As Integer
    'Set up buffer area for API function call return

    sShortPathName = Space(255)
    iLen = Len(sShortPathName)

    'Call the function

    lRetVal = GetLongPathName(sShortFileName, _
                        sShortPathName, iLen)
    'Strip away unwanted characters.

    Return Left(sShortPathName, lRetVal)
End Function

So now, in the code in Sub Main, rather than passing args(0) to frmSimpleFile, I now pass GetLongName(args(0)). Problem solved!

Points Of Interest

The application works by encrypting the given input file into a temporary output file (stored in the current user's local settings application data folder). It then deletes the original and moves the now encrypted file from the temporary directory to the location the original was kept.

Whilst creating this program, I kept experiencing difficulties when it came to the actual encryption and decryption. I kept getting extra null bytes added onto the end of a decrypted file, and also getting exceptions like "Invalid PKCS7 padding". After an annoyingly long period of debugging and kicking things , I realised that the reason for the second of those problems was due to the way I was closing and disposing of the CryptoStream object.

My original code in the Finally block on the end of the TransformFile method was as follows:

Finally
    If Not fsIn Is Nothing Then
        fsIn.Flush()
        fsIn.Close()
        fsIn = Nothing
    End If
    If Not fsOut Is Nothing Then
        fsOut.Flush()
        fsOut.Close()
        fsOut = Nothing
    End If
    'close the CryptoStream

    If Not encStream Is Nothing Then
        encStream.Close()
        encStream = Nothing
    End If
End Try

After changing that to the following, the padding problem was fixed. I think it was the order that was giving me problems - closing the underlying stream before closing encStream. I'm not sure, but I suppose the padding could be added during the Close method of the CryptoStream class. This means that closing the stream it sits on would mean the padding couldn't be added, and so decrypting the file would result in an exception (which was the problem I was having). If anyone does know for sure, I would be interested to hear from them.

Finally
     If Not encStream Is Nothing Then
         encStream.Close()
     End If
     If Not fsOut Is Nothing Then
         fsOut.Close()
     End If
     If Not fsIn Is Nothing Then
         fsIn.Close()
     End If
End Try

As for the extra null bytes, that was an altogether more obvious problem. I wrote a reasonable amount of the encryption code ages ago, when I was just starting with VB. My routine for encrypting a file consisted of using a 4 KB buffer to read in the input - as it still does, however, when it reads in a file, it puts the data into a (nulled) 4 KB array each time it reads in. It then encrypted the entire array. Foolishly, I forgot to store the number of bytes actually read in, and so the CryptoStream object would encrypt the extra null bytes as well. For this reason, a simple text file of one line would have loads of extra spaces attached to the end of it. Every decrypted file would be an exact multiple of 4096 bytes. Although I came up with a simple solution, it took an annoyingly long time to track the problem down. Sometimes, you think you know the code so well that when debugging, you simply see what you know should be there and subconsciously ignore what is clearly wrong .

To Do

The application works (in my opinion) pretty well as it is. There are, however, some items I would like to add at a later date:

  • Add folder encryption (right clicking a folder - encrypt every file in the folder).
  • Add multiple file encryption (selecting more than one file in Windows Explorer).

History

  • January 14th 2006 - Released.

Disclaimer

This program can be used to encrypt files. If you encrypt a file with this program, and then forget your password, there is no way you can get the file back - and it is not my fault!

If the program crashes whilst encrypting/decrypting and you lose valuable data, then I apologize, but I will not accept any blame.

THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here