Monday, September 26, 2011

Ensuring file is not tampered when used by .net code | For Thick Client Applications



Every now and then we use application files which are installed as part of your application on user’s machine. Recently, I came across the following scenario:

Scenario
We have a .net Think Client Application. Just take for example - We have a public key certificate (.cer) file which validates some signature. We want to ensure that this file is not tampered when we are using it. (I have just used certificate file as an example. It can be any file – say a pdf file which serves as a help file for my application.)

What is the issue?
As this file is part of our .net application which is installed on a user’s machine. User can change this certificate file with some of his own file, which may lead to security concerns. 

How to resolve the issue?
Let’s make it simple and split it into steps:
  1. We’ll embed the file in our assembly
  2.  Just before we want to use the file we’ll stream the file to user’s hard disk.
  3. The first time we stream the file we’ll store the MD5 hash value of the file’s content.
  4. Every time we use the file we’ll check the hash value:
a.       If it matches we are good to use the file
b.      If it doesn’t matches then we’ll delete this file and will go back to Step 2

Code for the solution
  1. Embedding file to the assembly : Add a “Resources” folder to your project. (optional – we’ll use this while retrieving file). Add the file Certificate.cer file to that folder and in the properties window of that file set the “Build Action” to “Embedded Resource".
  2. To retrieve and use this file use the following code :

  
namespace DefaultNamespace.FileHelper
{
    using System;
    using System.Configuration;
    using System.Diagnostics;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using System.Text;

    /// The Certificate File Helper Class
    public class CertificateFileHelper
    {
        /// 
        /// For entry in assembly
        /// 
        private static Assembly _entryAssembly;

        /// 
        /// Syncronization Root
        /// 
        private static object _syncRoot = new object();

        /// 
        /// Hash of the help file
        /// 
        private static string _hashCertificate;

        /// 
        /// Gets the Certificate File Resource Namespace
        /// 
        public const string CertificateFileResourceNamespace = ".Resources.CertificateFile.cer";

        /// 
        /// Gets the location of the Certificate file.
        /// 
        public const string CertificateFileLocation = @"Resources\CertificateFile.cer";

        /// 
        /// Get or Set Entry in Assembly
        /// 
        private static Assembly EntryAssembly
        {
            get
            {
                lock (_syncRoot)
                {
                    if (null == _entryAssembly)
                    {
                        _entryAssembly = Assembly.GetEntryAssembly();
                    }

                    // In a automated test scenario the Entry Assembly comes back null so grab yourself
                    if (null == _entryAssembly)
                    {
                        _entryAssembly = Assembly.GetExecutingAssembly();
                    }

                    return _entryAssembly;
                }
            }
            set
            {
                lock (_syncRoot)
                {
                    _entryAssembly = value;
                }
            }
        }

        /// 
        /// Checks if the digital signature on config are valid or not
        /// 
        /// Returns true if signatures are valid else returns false
        public static void DoWorkUsingCertificateFile()
        {
            // Your Code
            // Your Code
            // Your Code

            // Gets the cerficate file
            string file = GetCertificateFile();

            // Your Code
            // Your Code
            // Your Code
        }

        /// 
        /// Writes the cetificate file to disk if the file is not present.
        /// 
        /// Path at which file is written
        private static string GetCertificateFile()
        {
            // Certificate file location
            var certificateFile = string.Format(
                                    CultureInfo.CurrentCulture,
                                    "{0}\\{1}",
                                    Path.GetDirectoryName(EntryAssembly.Location),
                                    CertificateFileLocation);

            // Checks if certificate file already exists
            if (File.Exists(certificateFile))
            {
                // Returns the certificate file path if the hash file is not tampered
                if (!string.IsNullOrEmpty(_hashCertificate) && GetMD5Hash(GetBytesFromFile(certificateFile)).Equals(_hashCertificate))
                {
                    return certificateFile;
                }

                // Delete the file if the file is tampered
                File.Delete(certificateFile);
            }

            // Getting the default namespace
            // Common string across al the types that assembly has
            string defaultNamespace = EntryAssembly.GetTypes().Aggregate(
                                                                    string.Empty,
                                                                    (current, type) => string.IsNullOrEmpty(current) ? type.Namespace : GetCommonString(current, type.Namespace));

            // Getting the file stream embedded in the assembly using its resource information
            var certificateFileStream = EntryAssembly.GetManifestResourceStream(defaultNamespace + CertificateFileResourceNamespace);

            // Create a FileStream object to write a stream to a file
            using (FileStream fileStream = File.Create(certificateFile, (int)certificateFileStream.Length))
            {
                // Fill the bytes[] array with the stream data
                byte[] bytesInStream = new byte[certificateFileStream.Length];
                certificateFileStream.Read(bytesInStream, 0, bytesInStream.Length);

                // Storing the hash of the file
                _hashCertificate = GetMD5Hash(bytesInStream);

                // Use FileStream object to write to the specified file
                fileStream.Write(bytesInStream, 0, bytesInStream.Length);
            }

            return certificateFile;
        }


        /// 
        /// Gets the MD5 hash of the file.
        /// 
        /// The input bytes.</param>
        /// Hexadecimal hash string
        private static string GetMD5Hash(byte[] inputBytes)
        {
            var hashString = new StringBuilder();
            using (var cryptoServiceProvider = new MD5CryptoServiceProvider())
            {
                byte[] hash = cryptoServiceProvider.ComputeHash(inputBytes);

                foreach (byte b in hash)
                {
                    hashString.Append(b.ToString("x2").ToLower());
                }
            }
            return hashString.ToString();
        }


        /// 
        /// Gets the common string. (We are using this method for getting the default namespace)
        /// 
        /// The first string.</param>
        /// The second string.</param>
        /// common string
        private static string GetCommonString(string firstString, string secondString)
        {
            string commonString = string.Empty;
            int shortStringLength = firstString.Length < secondString.Length ? firstString.Length : secondString.Length;
            for (int i = 0; i < shortStringLength; i++)
            {
                if (!firstString[i].Equals(secondString[i]))
                {
                    break;
                }
                commonString = commonString + firstString[i];
            }

            // Removing trailing periods
            commonString = commonString.TrimEnd('.');

            return commonString;
        }

        /// 
        /// Gets the bytes from file.
        /// 
        /// The full file path.</param>
        /// File read in byte array
        private static byte[] GetBytesFromFile(string fullFilePath)
        {
            byte[] bytes;
            // this method is limited to 2^32 byte files (4.2 GB)
            using (FileStream fileStream = File.OpenRead(fullFilePath))
            {
                bytes = new byte[fileStream.Length];
                fileStream.Read(bytes, 0, Convert.ToInt32(fileStream.Length));
            }
            return bytes;
        }
    }
}

No comments:

Post a Comment