Click here to Skip to main content
Click here to Skip to main content

File Encryption/Decryption with Hash Verification in C#

By , 22 Oct 2004
 

Sample Image - FileEncryptDecrypt.jpg

Introduction

This article is an expansion on a few of the articles here on CodeProject. I noticed that there are a lot of articles and posts dealing with Cryptography in the .NET Framework. These were all well and good. They got me started. Then, as I was progressing and using the System.Security.Cryptography namespace, I noticed that if the file was the right size and padded correctly, even using a bad password would output a file. This was not acceptable to me. So, I set out to write a class that would allow me to encrypt and then decrypt/verify that the contents had been written correctly.

Background

These articles started me down the road of .NET Cryptography:

Since none of these verified the output, I wrote a class to fix this.

The Code

The EncryptFile method:

/// <summary>
/// This takes an input file and encrypts it into the output file>
/// </summary>
/// <param name="inFile">the file to encrypt</param>
/// <param name="outFile">the file to write the encrypted data to</param>
/// <param name="password">the password for use as the key</param>
/// <param name="callback">the method to call to notify of progress</param>
public static void EncryptFile(string inFile, string outFile, 
                    string password, CryptoProgressCallBack callback)
{
    using(FileStream fin = File.OpenRead(inFile),
          fout = File.OpenWrite(outFile))
    {
        long lSize = fin.Length; // the size of the input file for storing
        int size = (int)lSize;  // the size of the input file for progress
        byte[] bytes = new byte[BUFFER_SIZE]; // the buffer
        int read = -1; // the amount of bytes read from the input file
        int value = 0; // the amount overall read from the input file for progress

        // generate IV and Salt
        byte[] IV = GenerateRandomBytes(16);
        byte[] salt = GenerateRandomBytes(16);

        // create the crypting object
        SymmetricAlgorithm sma = CryptoHelp.CreateRijndael(password, salt);
        sma.IV = IV;            

        // write the IV and salt to the beginning of the file
        fout.Write(IV,0,IV.Length);
        fout.Write(salt,0,salt.Length);

        // create the hashing and crypto streams
        HashAlgorithm hasher = SHA256.Create();
        using(CryptoStream cout = new CryptoStream(fout,sma.CreateEncryptor(),
            CryptoStreamMode.Write),
              chash = new CryptoStream(Stream.Null,hasher,
                CryptoStreamMode.Write))
        {
            // write the size of the file to the output file
            BinaryWriter bw = new BinaryWriter(cout);
            bw.Write(lSize);

            // write the file cryptor tag to the file
            bw.Write(FC_TAG);

            // read and the write the bytes to the crypto stream 
            // in BUFFER_SIZEd chunks
            while( (read = fin.Read(bytes,0,bytes.Length)) != 0 )
            {
                cout.Write(bytes,0,read);
                chash.Write(bytes,0,read);    
                value += read;
                callback(0,size,value);
            }
            // flush and close the hashing object
            chash.Flush();
            chash.Close();

            // read the hash
            byte[] hash = hasher.Hash;

            // write the hash to the end of the file
            cout.Write(hash,0,hash.Length);

            // flush and close the cryptostream
            cout.Flush();
            cout.Close();
        }
    }
}

What is interesting about this method and makes it different than the other articles' methods, is the fact that I write out the IV and Salt to the beginning of the output file. This adds a little more security to the file. For more information on these terms, check out Ritter's Crypto Glossary. Then after those two arrays are written, I encrypt and write the file size and a special tag (arbitrarily generated by me). These allow for some simple verifications of the file. After this, I do the encryption of the file, while hashing the data. Once the input file is completely encrypted, I encrypt the hash and write it out. By putting the hash at the end, I am able to verify the contents after decryption.

The DecryptFile method:

/// <summary>
/// takes an input file and decrypts it to the output file
/// </summary>
/// <param name="inFile">the file to decrypt</param>
/// <param name="outFile">the to write the decrypted data to</param>
/// <param name="password">the password used as the key</param>
/// <param name="callback">the method to call to notify of progress</param>
public static void DecryptFile(string inFile, string outFile,
            string password, CryptoProgressCallBack callback)
{
    // NOTE:  The encrypting algo was so much easier...

    // create and open the file streams
    using(FileStream fin = File.OpenRead(inFile),
              fout = File.OpenWrite(outFile))
    {
        // the size of the file for progress notification
        int size = (int)fin.Length;
        // byte buffer
        byte[] bytes = new byte[BUFFER_SIZE];
        int read = -1; // the amount of bytes read from the stream
        int value = 0;
        int outValue = 0; // the amount of bytes written out

        // read off the IV and Salt
        byte[] IV = new byte[16];
        fin.Read(IV,0,16);
        byte[] salt = new byte[16];
        fin.Read(salt,0,16);

        // create the crypting stream
        SymmetricAlgorithm sma = CryptoHelp.CreateRijndael(password,salt);
        sma.IV = IV;

        value = 32; // the value for the progress
        long lSize = -1; // the size stored in the input stream

        // create the hashing object, so that we can verify the file
        HashAlgorithm hasher = SHA256.Create();

        // create the cryptostreams that will process the file
        using(CryptoStream cin = new CryptoStream(fin,sma.CreateDecryptor(),
                CryptoStreamMode.Read),
                  chash = new CryptoStream(Stream.Null,hasher,
                    CryptoStreamMode.Write))
        {
            // read size from file
            BinaryReader br = new BinaryReader(cin);
            lSize = br.ReadInt64();
            ulong tag = br.ReadUInt64();

            if(FC_TAG != tag)
                throw new CryptoHelpException("File Corrupted!");

            //determine number of reads to process on the file
            long numReads = lSize / BUFFER_SIZE;

            // determine what is left of the file, after numReads
            long slack = (long)lSize % BUFFER_SIZE;

            // read the buffer_sized chunks
            for(int i = 0; i < numReads; ++i)
            {
                read = cin.Read(bytes,0,bytes.Length);
                fout.Write(bytes,0,read);
                    chash.Write(bytes,0,read);
                value += read;
                outValue += read;
                callback(0,size,value);
            }

            // now read the slack
            if(slack > 0)
            {
                read = cin.Read(bytes,0,(int)slack);
                fout.Write(bytes,0,read);
                chash.Write(bytes,0,read);
                value += read;
                outValue += read;
                callback(0,size,value);
            }
            // flush and close the hashing stream
            chash.Flush();
            chash.Close();

            // flush and close the output file
            fout.Flush();
            fout.Close();

            // read the current hash value
            byte[] curHash = hasher.Hash;

            // get and compare the current and old hash values
            byte[] oldHash = new byte[hasher.HashSize / 8];
            read = cin.Read(oldHash,0,oldHash.Length);
            if((oldHash.Length != read) || (!CheckByteArrays(oldHash,curHash)))
                throw new CryptoHelpException("File Corrupted!");
        }
        
        // make sure the written and stored size are equal
        if(outValue != lSize)
            throw new CryptoHelpException("File Sizes don't match!");
    }
}

During decryption, I reverse the actions of encryption. First, I read both the IV and Salt from the file. I use these to create the SymmetricAlgorithm. Second, I decrypt and read the file size and the tag. This is the first step in verification--if the tag is equal to the const tag in the class, I know the file is so far not corrupted. Now comes the decryption of the file data. This took a little work, because normally I would just keep reading from the file until I could not read anymore. But I put the hash at the end. So, I had to figure out how to read only the amount of data in the file size. I did this by using a little math:

Number of Reads = The File Size / The Buffer Size
Left Over Bytes To Read = The File Size modulo The Buffer Size
NOTE: Both of these are integer math

Now, I use a for loop for reading most of the data, and then read the left over bytes.

During these reads, I hashed the decrypted data.

Then I read off the hash that was written last and compared it to the newly created hash. If they were equal, the file was not corrupted and the correct password was used to decrypt the file. If not, the algorithm has caught the error.

Using the code

This code is pretty easy to use:

using nb;
public class TestClass
{
    string myPassword = "TESTING!@#_123__";
    string myPlainFile = "test.txt";
    string myEncryptedFile = "test.encrypted";
    string myDecryptedFile = "test.decrypted";

    private static void Callback(int min, int max, int value)
    {
        // do something with progress (progress bar or just ignore it)
    }

    [STAThread]
    static void Main()
    {
        CryptoProgressCallBack cb = new CryptoProgressCallBack(Callback);
        //Do the Encryption
        CryptoHelp.EncryptFile(myPlainFile, myEncryptedFile, myPassword, cb);
        //Do the decryption
        CryptoHelp.DecryptFile(myEncryptedFile,myDecryptedFile, myPassword, cb);
    }
}

Points of Interest

The things I learned were how to use the .NET Framework's hashing/crypting algorithms. These classes when used in conjunction allow for some pretty powerful data security and data verification. I was able to verify the files data with a minimum overhead.

I hope that this is a help to others who need to add some security to their applications.

History

  • 22 Oct 2004: 1.0 - Initial release.

    License

    This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

    About the Author

    Nathan Blomquist
    Software Developer
    United States United States
    Member
    No Biography provided

    Sign Up to vote   Poor Excellent
    Add a reason or comment to your vote: x
    Votes of 3 or less require a comment

    Comments and Discussions

     
    You must Sign In to use this message board.
    Search this forum  
        Spacing  Noise  Layout  Per page   
    QuestionArgument out of range exception for big files?membera.ajames9 May '12 - 12:18 
    This is working perfectly, for small files.
    but i cannot encrypt files with greater size such as 4 GB,
    "Argument out of range exception" at file "frmFileEncryptDecryptMain.cs" line number 322. what is the problem?
    any way great code! Smile | :)
    BugAttention:Major Bug!memberSrikanth Basappa23 Apr '12 - 16:52 
    Hello there,
    This is a great approach to file encryption. But I have recently identified a major issue with implementation.
     
    BUG:
    In the Decryption function. The bytes array is never cleared after reading the buffer_sized chunks. As such if there is any slack that need to be read it will get added to beginning of the bytes array and potentially will have duplicate data. It was causing my application to behave rather strangely over a period of time and I narrowed it down to this issue. Here is an example of what is going on:
    Say buffer_size = 3;
    bytes to read = 4;
     
    result after reading from buffer_sized chunks.
    Buffer[0] = data1
    Buffer[1] = data2
    Buffer[2] = data3
     
    result after reading from slack:
    Buffer[0] = data4
    Buffer[1] = data2
    Buffer[2] = data3
     
    Output file:
    data1
    data2
    data3
    data4
    data2
    data3
    Expected Result:
    data1
    data2
    data3
    data4
     

    To fix this I am not using a static sized buffer anymore. I am using the lsize variable during encryption and decryption. That should fix this issue and probably will not need to read buffer_sized chunks and slack separately.
     
    Hope this is helpful Wink | ;)
     
    cheers,
    Srikanth.
    GeneralRe: Attention:Major Bug!memberray_man197911 Apr '13 - 3:27 
    This bug can be fixed by replacing "fout = File.OpenWrite(outFile)" on "fout = File.Create(outFile)" in both functions EncryptFile and DecryptFile.
    GeneralMy vote of 5membermanoj kumar choubey29 Mar '12 - 23:30 
    Nice
    Generalsorrymembernashwann16 Aug '10 - 14:33 
    hello
     
    i'm not perfect in cryptography field ..
     
    so can i know .. what is the type of this application
     
    i know DES , AES , RC4,5,6 , ....
     
    so what is the name of this algorithm ???
    Questioncan i use this in a Web Application?membersuresh_kumar_s7 Dec '09 - 19:21 
    Hi can i use this in a Web Application?
    Thanks
    Surez
     
    sureshkumaran

    AnswerRe: can i use this in a Web Application?memberNathan Blomquist12 Dec '09 - 11:59 
    Are you asking if the code will work in a web application? Or are you asking permission?
     
    It should work fine in a web application. Remember, this is just demo code; no guarantee that it won't format your hard drive Laugh | :laugh: .
     
    ---------------------------
    Hmmm... what's a signature?

    GeneralRe: can i use this in a Web Application?membersuresh_kumar_s13 Dec '09 - 18:15 
    mmm...gud reply(!!??) Laugh | :laugh:
     
    sureshkumaran

    GeneralSome small bugfixesmemberKim Togo15 Oct '09 - 4:28 
    Hi
     
    Great encryption class!
     
    I got some small bugfixes for the class.
     
    Under EncryptFile in CryptoHelp.cs
     
    Change int size to long size and int value to long value.
     
    Under DecryptFile in CryptoHelp.cs
     
    Change int size to long size, int value to long value and int outValue to long outValue.
     
    And of course change the delegate void CryptoProgressCallBack to long.
     
    This will support encryption and decryption of files larger than 4GB.
    QuestionI need help about Encryp and DesEncryp Tiff FilesmemberDiego Maciado14 May '09 - 7:44 
    Thanks for you code. It´s really good.
     
    I have a problem and I can´t find the solution. Can you help me?
     
    I used the method EncryptFile with a xxx.tiff file and work OK.
    Then I used the method DecryptFile with a xxx.tiff.enc file and work OK.
     
    Problem:
     
    When I used the file: tiff´s DecryptFile and try to join to other tiff files (I try to add more tiff to a document to join in a only one m-tiff file), Then I used the method EncryptFile and apparently work OK.
     
    But
     
    When I used the method DecryptFile say that the: File Corrupted!
     
    if(FC_TAG != tag)
    throw new CryptoHelpException("File Corrupted!");
     
    the FC_TAG = 18158797384510146255 ulong
    the tag = 17843326550202218999 ulong
     
    And I used the same method and TAG flag:
    private const ulong FC_TAG = 0xFC010203040506CF; same in decimal 18158797384510146255
     
    Thank for your help
     
    Diego Maciado

    General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

    Permalink | Advertise | Privacy | Mobile
    Web01 | 2.6.130523.1 | Last Updated 22 Oct 2004
    Article Copyright 2004 by Nathan Blomquist
    Everything else Copyright © CodeProject, 1999-2013
    Terms of Use
    Layout: fixed | fluid