Share via


Security

This chapter is excerpted from C# 3.0 Cookbook, Third Edition: More than 250 solutions for C# 3.0 programmers by Jay Hilyard, Stephen Teilhet, published by O'Reilly Media

C# 3.0 Cookbook, Third Edition

Logo

Buy Now

Introduction

There are many ways to secure different parts of your application. The security of running code in .NET revolves around the concept of Code Access Security (CAS). CAS determines the trustworthiness of an assembly based upon its origin and the characteristics of the assembly itself, such as its hash value. For example, code installed locally on the machine is more trusted than code downloaded from the Internet. The runtime will also validate an assembly's metadata and type safety before that code is allowed to run.

There are many ways to write secure code and protect data using the .NET Framework. In this chapter, we explore such things as controlling access to types, encryption and decryption, random numbers, securely storing data, and using programmatic and declarative security.

Controlling Access to Types in a Local Assembly

Problem

You have an existing class that contains sensitive data, and you do not want clients to have direct access to any objects of this class. Instead, you want an intermediary object to talk to the clients and to allow access to sensitive data based on the client's credentials. What's more, you would also like to have specific queries and modifications to the sensitive data tracked, so that if an attacker manages to access the object, you will have a log of what the attacker was attempting to do.

Solution

Use the proxy design pattern to allow clients to talk directly to a proxy object. This proxy object will act as gatekeeper to the class that contains the sensitive data. To keep malicious users from accessing the class itself, make it private, which will at least keep code without the ReflectionPermissionFlag. MemberAccess access (which is currently given only in fully trusted code scenarios such as executing code interactively on a local machine) from getting at it.

The namespaces we will be using are:

   using System;
    using System.IO;
    using System.Security;
    using System.Security.Permissions;
    using System.Security.Principal;

Let's start this design by creating an interface, as shown in Example 17-1, "ICompanyData interface", that will be common to both the proxy objects and the object that contains sensitive data.

Example 17-1. ICompanyData interface

internal interface ICompanyData
{
    string AdminUserName
    {
        get;
        set;
    }

    string AdminPwd
    {
        get;
        set;
    }
    string CEOPhoneNumExt
    {
        get;
        set;
    }
    void RefreshData();
    void SaveNewData();
}

The CompanyData class shown in Example 17-2, "CompanyData class" is the underlying object that is "expensive" to create.

Example 17-2. CompanyData class

internal class CompanyData : ICompanyData
{
    public CompanyData()
    {
        Console.WriteLine("[CONCRETE] CompanyData Created");
        // Perform expensive initialization here.
        this.AdminUserName ="admin";
        this.AdminPd ="password";

        this.CEOPhoneNumExt ="0000";
    }

    public str ing AdminUserName
    {
        get;
        set;
    }

    public string AdminPwd
    {
        get;
        set;
    }

    public string CEOPhoneNumExt
    {
        get;
        set;
    }

    public void RefreshData()
    {
        Console.WriteLine("[CONCRETE] Data Refreshed");
    }

    public void SaveNewData()
    {
        Console.WriteLine("[CONCRETE] Data Saved");
    }
}

The code shown in Example 17-3, "CompanyDataSecProxy security proxy class" for the security proxy class checks the caller's permissions to determine whether the CompanyData object should be created and its methods or properties called.

Example 17-3. CompanyDataSecProxy security proxy class

public class CompanyDataSecProxy : ICompanyData
{
    public CompanyDataSecProxy()
    {
        Console.WriteLine("[SECPROXY] Created");

        // Must set principal policy first.
        appdomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.
           WindowsPrincipal);
    }

    private ICompanyData coData = null;
    private PrincipalPermission admPerm =
        new PrincipalPermission(null, @"BUILTIN\Administrators", true);
    private PrincipalPermission guestPerm =
        new Pr incipalPermission(null, @"BUILTIN\Guest", true);
    private PrincipalPermission powerPerm =
        new PrincipalPermission(null, @"BUILTIN\PowerUser", true);
    private PrincipalPermission userPerm =
        new PrincipalPermission(null, @"BUILTIN\User", true);

    public string AdminUserName
    {
        get
        {
            string userName = ";
            try
            {
                admPerm.Demand();
                Startup();
                userName =coData.AdminUserName;
            }
            catch(SecurityException e)
            {
                Console.WriteLine("AdminUserName_get failed! {0}",e.ToString());
            }
            return (userName);
        }
        set
            {
            try
            {
                admPerm.Demand();
                Startup();
                coData.AdminUserName = value;
            }
            catch(SecurityException e)
            {
                Console.WriteLine("AdminUserName_set failed! {0}",e.ToString());
            }
        }
    }

    public string AdminPwd
    {
        get
        {
            string pwd = ";
            try
            {
                admPerm.Demand();
                Startup();
                pwd = coData.AdminPwd;
            }
            catch(SecurityException e)
            {
                Console.WriteLine("AdminPwd_get Failed! {0}",e.ToString());
            }

            return (pwd);
        }
        set
        {
            try
            {
                admPerm.Demand();
                Startup();
                coData.AdminPwd = value;
            }
            catch(SecurityException e)
            {
                Console.WriteLine("AdminPwd_set Failed! {0}",e.ToString());
            }
        }
    }

    public string CEOPhoneNumExt
    {
        get
        {
            string ceoPhoneNum = ";
            try
            {
                admPerm.Union(powerPerm).Demand();
                Startup();
                ceoPhoneNum = coData.CEOPhoneNumExt;
            }
            catch(SecurityException e)
            {
                Console.WriteLine("CEOPhoneNum_set Failed! {0}",e.ToString());
            }
            return (ceoPhoneNum);
        }
        set
        {
            try
            {
                admPerm.Demand();
                Startup();
                coData.CEOPhoneNumExt = value;
            }
            catch(SecurityException e)
            {
                Console.WriteLine("CEOPhoneNum_set Failed! {0}",e.ToString());
            }
        }
    }
    public void RefreshData()
    {
        try
        {
            admPerm.Union(powerPerm.Union(userPerm)).Dem and();
            Startup();
            Console.WriteLine("[SECPROXY] Data Refreshed");
            coData.RefreshData();
        }
        catch(SecurityException e)
        {
            Console.WriteLine("RefreshData Failed! {0}",e.ToString());
        }
    }

    public void SaveNewData()
    {
        try
        {
            admPerm.Union(powerPerm).Demand();
            Startup();
            Console.WriteLine("[SECPROXY] Data Saved");
            coData.SaveNewData();
        }
        catch(SecurityException e)
        {
            Console.WriteLine("SaveNewData Failed! {0}",e.ToString());
        }
    }

    // DO NOT forget to use [#define DOTRACE] to control the tracing proxy.
    private void Startup()
    {
        if (coData == null)
        {
#if (DOTRACE)
            coData = new CompanyDataTraceProxy();
#else
            coData = new CompanyData();
#endif
            Console.WriteLine("[SECPROXY] Refresh Data");
            coData.RefreshData();
        }
    }
}

When creating thePrincipalPermissions as part of the object construction, you are using string representations of the built-in objects ("BUILTIN\Administrators") to set up the principal role. However, the names of these objects may be different depending on the locale the code runs under. It would be appropriate to use the WindowsAccountType.Administrator enumeration value to ease localization because this value is defined to represent the administrator role as well. We used text here to clarify what was being done and also to access the PowerUsers role, which is not available through the WindowsAccountType enumeration.

If the call to the CompanyData object passes through the CompanyDataSecProxy, then the user has permissions to access the underlying data. Any access to this data may be logged, so the administrator can check for any attempt to hack the CompanyData object. The code shown in Example 17-4, "CompanyDataTraceProxy tracing proxy class" is the tracing proxy used to log access to the various method and property access points in the CompanyData object (note that the CompanyDataSecProxy contains the code to turn this proxy object on or off).

Example 17-4. CompanyDataTraceProxy tracing proxy class

public class CompanyDataTraceProxy : ICompanyData
{    
    public CompanyDataTraceProxy() 
    {
        Console.WriteLine("[TRACEPROXY] Created");
        string path = Path.GetTempPath() + @"\CompanyAccessTraceFile.txt";
        fileStream = new FileStream(path, FileMode.Append,
            FileAccess.Write, FileShare.None);
        traceWriter = new StreamWriter(fileStream);
        coData = new CompanyData();
    }

    private ICompanyData coData = null;
    private FileStream fileStream = null;
    private StreamWriter traceWriter = null;

    public string AdminPwd
    {
        get
        {
            traceWriter.WriteLine("AdminPwd read by user.");
            traceWriter.Flush();
            return (coData.AdminPwd);
        }
        set
        {
            traceWriter.WriteLine("AdminPwd written by user.");
            traceWriter.Flush();
            coData.AdminPwd = value;
        }
    }

    public string AdminUserName
    {
        get
        {
            traceWriter.WriteLine("AdminUserName read by user.");
            traceWriter.Flush();
            return (coData.AdminUserName);
        }
        set
        {
            traceWriter.WriteLine("AdminUserName written by user.");
            traceWriter.Flush(); 
            coData.AdminUserName = value;
        }
    }

    public string CEOPhoneNumExt
    {
        get
        {
            traceWriter.WriteLine("CEOPhoneNumExt read by user.");
            traceWriter.Flush();
            return (coData.CEOPhoneNumExt);
        }
        set
        {
            traceWriter.WriteLine("CEOPhoneNumExt written by user.");
            traceWriter.Flush();
            coData.CEOPhoneNumExt = value;
        }
    }

    public void RefreshData()
    {
        Console.WriteLine("[TRACEPROXY] Refresh Data");
        coData.RefreshData();
    }

    public void SaveNewData()
    {
        Console.WriteLine("[TRACEPROXY] Save Data");
        coData.SaveNewData();
    }
}

The proxy is used in the following manner:

  // Create the security proxy here.
    CompanyDataSecProxy companyDataSecProxy = new CompanyDataSecProxy( );

    // Read some data.
    Console.WriteLine("CEOPhoneNumExt: " + companyDataSecProxy.CEOPhoneNumExt);

    // Write some data.
    companyDataSecProxy.AdminPwd = "asdf";
    companyDataSecProxy.AdminUserName = "asdf";

    // Save and refresh this data.
    companyDataSecProxy.SaveNewData( );
    companyDataSecProxy.RefreshData( );

Note that as long as the CompanyData object was accessible, you could have also written this to access the object directly:

   // Instantiate the CompanyData object directly without a proxy.
    CompanyData companyData = new CompanyData( );

    // Read some data.
    Console.WriteLine("CEOPhoneNumExt: " + companyData.CEOPhoneNumExt);

    // Write some data.
    companyData.AdminPwd = "asdf";
    companyData.AdminUserName = "asdf";

    // Save and refresh this data.
    companyData.SaveNewData();
    companyData.RefreshData();

If these two blocks of code are run, the same fundamental actions occur: data is read, data is written, and data is updated/refreshed. This shows you that your proxy objects are set up correctly and function as they should.

Discussion

The proxy design pattern is useful for several tasks. The most notable-in COM, COM+, and .NET remoting-is for marshaling data across boundaries such as AppDomains or even across a network. To the client, a proxy looks and acts exactly the same as its underlying object; fundamentally, the proxy object is just a wrapper around the object.

A proxy can test the security and/or identity permissions of the caller before the underlying object is created or accessed. Proxy objects can also be chained together to form several layers around an underlying object. Each proxy can be added or removed depending on the circumstances.

For the proxy object to look and act the same as its underlying object, both should implement the same interface. The implementation in this recipe uses an ICompanyData interface on both the proxies (CompanyDataSecProxy and CompanyDataTraceProxy) and the underlying object (CompanyData). If more proxies are created, they, too, need to implement this interface.

The CompanyData class represents an expensive object to create. In addition, this class contains a mixture of sensitive and nonsensitive data that requires permission checks to be made before the data is accessed. For this recipe, the CompanyData class simply contains a group of properties to access company data and two methods for updating and refreshing this data. You can replace this class with one of your own and create a corresponding interface that both the class and its proxies implement.

The CompanyDataSecProxy object is the object that a client must interact with. This object is responsible for determining whether the client has the correct privileges to access the method or property that it is calling. The get accessor of the AdminUserName property shows the structure of the code throughout most of this class:

  public string AdminUserName
    {
        get
        {
            string userName = ";
            try
            {
                admPerm.Demand( );
                Startup( );
                userName = coData.AdminUserName;
            }
            catch(SecurityException e)
            {
               Console.WriteLine("AdminUserName_get ailed!: {0}",e.ToString( ));
            }
            return (userName);
        }
        set
        {
            try
            {
                admPerm.Demand( );
                Startup( );
                coData.AdminUserName = value;
            }
            catch(SecurityException e)
            {
               Console.WriteLine("AdminUserName_set Failed! {0}",e.ToString( ));
            }
        }
    }

Initially, a single permission (AdmPerm) is demanded. If this demand fails, a SecurityException, which is handled by the catch clause, is thrown. (Other exceptions will be handed back to the caller.) If the Demand succeeds, the Startup method is called. It is in charge of instantiating either the next proxy object in the chain (CompanyDataTraceProxy) or the underlying CompanyData object. The choice depends on whether the DOTRACE preprocessor symbol has been defined. You may use a different technique, such as a registry key to turn tracing on or off, if you wish.

This proxy class uses the private field coData to hold a reference to an ICompanyData type, which can be either a CompanyDataTraceProxy or the CompanyData object. This reference allows you to chain several proxies together.

The CompanyDataTraceProxy simply logs any access to the CompanyData object's information to a text file. Since this proxy will not attempt to prevent a client from accessing the CompanyData object, the CompanyData object is created and explicitly called in each property and method of this object.

See Also

Design Patterns by Gamma et al. (Addison-Wesley).

Encrypting/Decrypting a String

Problem

You have a string you want to be able to encrypt and decrypt-perhaps a password or software key-which will be stored in some form, such as in a file or the registry. You want to keep this string a secret so that users cannot take this information from you.

Solution

Encrypting the string will help to prevent users from being able to read and decipher the information. The CryptoString class shown in Example 17-5, "CryptoString class" contains two static methods to encrypt and decrypt a string and two static properties to retrieve the generated key and initialization vector (IV-a random number used as a starting point to encrypt data) after encryption has occurred.

Example 17-5. CryptoString class

using System;
using System.Security.Cryptography;

public sealed class CryptoString
{
    private CryptoString() {}

    private static byte[] savedKey = null;
    private static byte[] savedIV = null;

     public static byte[] Key
     {
      get { return savedKey; }
      set { savedKey = value; }
     }

     public static byte[] IV
     {
      get { return savedIV; }
      set { savedIV = value; }
     }

    private static void RdGenerateSecretKey(RijndaelManaged rdProvider)
    {
        if (savedKey == null)
        {
            rdProvider.KeySize = 256;
            rdProvider.GenerateKey( );
            savedKey = rdProvider.Key;
        }
    }

    private static void RdGenerateSecretInitVector(RijndaelManaged rdProvider)<
    {
        if (savedIV == null)
        {
            rdProvider.GenerateIV();
            savedIV = rdProvider.IV;
        }
    }

    public static string Encrypt(string originalStr)
    {
        // Encode data string to be stored in memory.
        byte[] originalStrAsBytes = Encoding.ASCII.GetBytes(originalStr);
        byte[] originalBytes = {};

        // Create MemoryStream to contain output.
        using (MemoryStream memStream = new
                 MemoryStream(originalStrAsBytes.Length))
        {
            using (RijndaelManaged rijndael = new RijndaelManaged()) 
            {
                // Generate and save secret key and init vector.
                RdGenerateSecretKey(rijndael);
                RdGenerateSecretInitVector(rijndael);

                if (savedKey == null || savedIV == null)
                {
                    throw (new NullReferenceException( 
                            "savedKey and savedIV must be non-null.")); 
                }

                // Create encryptor and stream objects.
                using (ICryptoTransform rdTransform = 
                       rijndael.CreateEncryptor((byte[])savedKey.
                       Clone(),(byte[])savedIV.Clone()))
                {
                    using (CryptoStream cryptoStream = new CryptoStream(memStream,
                          rdTransform, CryptoStreamMode.Write))
                    {
                        // Write encrypted data to the MemoryStream.
                        cryptoStream.Write(originalStrAsBytes, 0,
                                   originalStrAsBytes.Length);
                        cryptoStream.FlushFinalBlock();
                        originalBytes = memStream.ToArray();
                    }
                }
            }

        }
        // Convert encrypted string.
        string encryptedStr = Convert.ToBase64String(originalBytes);
        return (encryptedStr);
    }

    public static string Decrypt(string encryptedStr)
    {
        // Unconvert encrypted string.
        byte[] encryptedStrAsBytes = Convert.FromBase64String(encryptedStr);
        byte[] initialText = new Byte[encryptedStrAsBytes.Length];

        using (RijndaelManaged rijndael = new RijndaelManaged())
        { 
            using (MemoryStream memStream = new MemoryStream(encryptedStrAsBytes)) 
            {
                if (savedKey == null || savedIV == null) 
                {
                    throw (new NullReferenceException(
                            "savedKey and savedIV must be non-null."));
                }

                // Create decryptor and stream objects.
                using (ICryptoTransform rdTransform = 
                     rijndael.CreateDecryptor((byte[])savedKey.
                     Clone(),(byte[])savedIV.Clone()))
                {
                    using (CryptoStream cryptoStream = new CryptoStream(memStream, 
                     rdTransform, CryptoStreamMode.Read))
                     {
                     // Read in decrypted string as a byte[].
                     cryptoStream.Read(initialText, 0, initialText.Length);
                     }
                }
            }
        }

        // Convert byte[] to string.
        string decryptedStr = Encoding.ASCII.GetString(initialText);
        return (decryptedStr);
    }
}

Discussion

The CryptoString class contains only static members, except for the private instance constructor, which prevents anyone from directly creating an object from this class.

This class uses the Rijndael algorithm to encrypt and decrypt a string. This algorithm is found in the System.Security.Cryptography.RijndaelManaged class. This algorithm requires a secret key and an initialization vector; both are byte arrays. A random secret key can be generated for you by calling the GenerateKey method on the RijndaelManaged class. This method accepts no parameters and returns void. The generated key is placed in the Key property of the RijndaelManaged class. The GenerateIV method generates a random initialization vector and places this vector in the IV property of the RijndaelManaged class.

The byte array values in the Key and IV properties must be stored for later use and not modified. This is due to the nature of private-key encryption classes, such as RijndaelManaged. The Key and IV values must be used by both the encryption and decryption routines to successfully encrypt and decrypt data.

The SavedKey and SavedIV private static fields contain the secret key and initialization vector, respectively. The secret key is used by both the encryption and decryption methods to encrypt and decrypt data. This is why there are public properties for these values, so they can be stored somewhere secure for later use. This means that any strings encrypted by this object must be decrypted by this object. The initialization vector is there to make deducing the secret key from the encrypted string much more difficult. The initialization vector does this by causing two identical encrypted strings (encrypted with the same key) to look very different in their encrypted form.

Two methods in the CryptoStringclass, RdGenerateSecretKey and RdGenerateSecretInitVector, are used to generate a secret key and initialization vector when none exists. The RdGenerateSecretKey method generates the secret key, which is placed in the SavedKey field. Likewise, the RdGenerateSecretInitVector generates the initialization vector, which is placed in the SavedIVfield. There is only one key and one IV generated for this class. This enables the encryption and decryption routines to have access to the same key and IV information at all times.

The Encrypt and Decrypt methods of the CryptoString class do the actual work of encrypting and decrypting a string. The Encrypt method accepts a string that you want to encrypt and returns an encrypted string. The following code calls this method and passes in a string to be encrypted:

   string encryptedString = CryptoString.Encrypt("MyPassword");
    Console.WriteLine("encryptedString: {0}", encryptedString);
    // Get the key and IV used so you can decrypt it later.
    byte [] key = CryptoString.Key;
    byte [] IV = CryptoString.IV;

Once the string is encrypted, the key and IV are stored for later decryption. This method displays:

 encryptedString: Ah4vkmVKpwMYRT97Q8cVgQ==

Note that your output may differ since you will be using a different key and IV value. The following code sets the key and IV used to encrypt the string and then calls the Decrypt method to decrypt the previously encrypted string:

  CryptoString.Key = key;
    CryptoString.IV = IV;
    string decryptedString = CryptoString.Decrypt(encryptedString);
    Console.WriteLine("decryptedString: {0}", decryptedString);

This method displays:

 decryptedString: MyPassword

There does not seem to be any problem with using escape sequences such as \r, \n, \r\n, or \t in the string to be encrypted. In addition, using a quoted string literal, with or without escaped characters, works without a problem:

 @"MyPassword"

See Also

the section called "Encrypting and Decrypting a File"; the "System.Cryptography Namespace," "MemoryStream Class," "ICryptoTransform Interface," and "RijndaelManaged Class" topics in the MSDN documentation.

Encrypting and Decrypting a File

Problem

You have sensitive information that must be encrypted before it is written to a file that might be in a nonsecure area. This information must also be decrypted before it is read back in to the application.

Solution

Use multiple cryptography providers and write the data to a file in encrypted format. This is accomplished in the following class, which has a constructor that expects an instance of the System.Security.Cryptography.SymmetricAlgorithm class and a path for the file. The SymmetricAlgorithm class is an abstract base class for all cryptographic providers in .NET, so you can be reasonably assured that this class could be extended to cover all of them. This example implements support for TripleDES and Rijndael.

The following namespaces are needed for this solution:

   using System;
    using System.Text;
    using System.IO;
    using System.Security.Cryptography;

The class SecretFile (implemented in this recipe) can be used for TripleDES as shown:

         // Use TripleDES.
            using (TripleDESCryptoServiceProvider tdes = new
    TripleDESCryptoServiceProvider())

            {
                SecretFile secretTDESFile = new SecretFile(tdes,"tdestext.secret");

                string encrypt = "My TDES Secret Data!";
                Console.WriteLine("Writing secret data: {0}",encrypt);
                secretTDESFile.SaveSensitiveData(encrypt);
                // Save for storage to read file.
                byte [] key = secretTDESFile.Key;
                byte [] IV = secretTDESFile.IV;

                string decrypt = secretTDESFile.ReadSensitiveData( );
                Console.WriteLine("Read secret data: {0}",decrypt);
            }

To use SecretFile with Rijndael, just substitute the provider in the constructor like this:

 // Use Rijndael.
    using (RijndaelManaged rdProvider = new RijndaelManaged( ))
    {
        SecretFile secretRDFile = new SecretFile(rdProvider,"rdtext.secret");

        string encrypt = "My Rijndael Secret Data!";

        Console.WriteLine("Writing secret data: {0}",encrypt);
        secretRDFile.SaveSensitiveData(encrypt);
        // Save for storage to read file.
        byte [] key = secretRDFile.Key;
        byte [] IV = secretRDFile.IV;

        string decrypt = secretRDFile.ReadSensitiveData( );
        Console.WriteLine("Read secret data: {0}",decrypt);
    }

Example 17-6, "SecretFile class" shows the implementation of SecretFile.

Example 17-6. SecretFile class

public class SecretFile
{
    private byte[] savedKey = null;
    private byte[] savedIV = null;
    private SymmetricAlgorithm symmetricAlgorithm;
    string path;

    public byte[] Key
    {
        get { return savedKey; }
        set { savedKey = value; }
    }

    public byte[] IV
    {
        get { return savedIV; }
        set { savedIV = value; }
    }

    public SecretFile(SymmetricAlgorithm algorithm, string fileName)
    {
        symmetricalgorithm;
        path = fileName;
    }

    public void SaveSensitiveData(string sensitiveData)
    {
        // Encode data string to be stored in encrypted file.
        byte[] encodedData = Encoding.Unicode.GetBytes(sensitiveData);

        // Create FileStream and crypto service provider objects.
        using (FileStream fileStream = new FileStream(path,
                                               FileMode.Create,
                                               FileAccess.Write))
        {
            // Generate and save secret key and init vector.
            GenerateSecretKey( );
            GenerateSecretInitVector( );

            // Create crypto transform and stream objects.
            using (ICryptoTransform transform =
                        symmetricAlgorithm.CreateEncryptor(savedKey,
                        savedIV))
            {
                using (CryptoStream cryptoStream =
              new CryptoStream(fileStream, transform, CryptoStreamMode.Write))
                {
                    // Write encrypted data to the file.
                    cryptoStream.Write(encodedData, 0, encodedData.Length);
                }
            }
        }
    }

    public string ReadSensitiveData( )
    {
        string decrypted = ";

        // Create file stream to read encrypted file back.
        using (FileStream fileStream = new FileStream(path,
                                               FileMode.Open,
                                               FileAccess.Read))
        {
            // Print out the contents of the encrypted file.
            using (BinaryReader binReader = new BinaryReader(fileStream))
            {
                Console.WriteLine("---------- Encrypted Data ---------");
                int count = (Convert.ToInt32(binReader.BaseStream.Length));
                byte [] bytes = binReader.ReadBytes(count);
                char [] array = Encoding.Unicode.GetChars(bytes);
                string encdata = new string(array);
                Console.WriteLine(encdata);
                Console.WriteLine("---------- Encrypted Data ---------\r\n");

                // Reset the file stream.
                fileStream.Seek(0,SeekOrigin.Begin);

                // Create decryptor.
                using (ICryptoTransform transform =
                    symmetricAlgorithm.CreateDecryptor(savedKey, savedIV))
                {
                    using (CryptoStream cryptoStream = new CryptoStream(fileStream,
                                                     transform,
                                                     CryptoStreamMode.Read))
                    {
                        // Print out the contents of the decrypted file.
                        StreamReader srDecrypted = new StreamReader(cryptoStream,
                                                    new UnicodeEncoding( ));
                        Console.WriteLine("---------- Decrypted Data ---------");
                        decrypted = srDecrypted.ReadToEnd( );
                        Console.WriteLine(decrypted);
                        Console.WriteLine("---------- Decrypted Data ---------");
                    }
                }
            }
        }

        return decrypted;
    }

    private void GenerateSecretKey( )
    {
        if (null != (symmetricAlgorithm as TripleDESCryptoServiceProvider))
        {
            TripleDESCryptoServiceProvider tdes;
            tdes = symmetricAlgorithm as TripleDESCryptoServiceProvider;
            tdes.KeySize = 192; // Maximum key size
            tdes.GenerateKey( );
            savedKey = tdes.Key;
        }
        else if (null != (symmetricAlgorithm as RijndaelManaged))
        {
            RijndaelManaged rdProvider;
            rdProvider = symmetricAlgorithm as RijndaelManaged;
            rdProvider.KeySize = 256; // Maximum key size
            rdProvider.GenerateKey( );
            savedKey = rdProvider.Key;
        }
    }

    private void GenerateSecretInitVector( )
    {
        if (null != (symmetricAlgorithm as TripleDESCryptoServiceProvider))
        {
            TripleDESCryptoServiceProvider tdes;
            tdes = symmetricAlgorithm as TripleDESCryptoServiceProvider;
            tdes.GenerateIV( );
            savedIV = tdes.IV;
        }
        else if (null != (symmetricAlgorithm as RijndaelManaged))
        {
            RijndaelManaged rdProvider;
            rdProvider = symmetricAlgorithm as RijndaelManaged;
            rdProvider.GenerateIV( );
            savedIV = rdProvider.IV;
        }
    }

If the SaveSensitiveData method is used to save the following text to a file:Algorithm =

   This is a test
    This is sensitive data!

the ReadSensitiveData method will display the following information from this same file:

  ---------- Encrypted Data --------
    ????????????????????????????????????????
    ---------- Encrypted Data --------

    ---------- Decrypted Data ---------
    This is a test
    This is sensitive data!
    ---------- Decrypted Data ---------

Discussion

Encrypting data is essential to many applications, especially ones that store information in easily accessible locations. Once data is encrypted, a decryption scheme is required to restore the data back to an unencrypted form without losing any information. The same underlying algorithms can be used to authenticate the source of a file or message.

The encryption schemes used in this recipe are TripleDES and Rijndael. The reasons for using Triple DES are:

  • TripleDES employs symmetric encryption, meaning that a single private key is used to encrypt and decrypt data. This process allows much faster encryption and decryption, especially as the streams of data become larger.

  • TripleDES encryption is much harder to crack than the older DES encryption and is widely considered to be of high strength.

  • If you wish to use another type of encryption, this recipe can be easily converted using any provider derived from the SymmetricAlgorithm class.

  • TripleDES is widely deployed in the industry today.

The main drawback to TripleDES is that both the sender and receiver must use the same key and initialization vector (IV) in order to encrypt and decrypt the data successfully. If you wish to have an even more secure encryption scheme, use the Rijndael scheme. This type of encryption scheme is highly regarded as a solid encryption scheme, since it is fast and can use larger key sizes than TripleDES. However, it is still a symmetric cryptosystem, which means that it relies on shared secrets. Use an asymmetric cryptosystem, such as RSA or DSA, for a cryptosystem that uses shared public keys with private keys that are never shared between parties.

See Also

The "SymmetricAlgorithm Class," "TripleDESCryptoServiceProvider Class," and "RijndaelManaged Class" topics in the MSDN documentation.

Cleaning Up Cryptography Information

Problem

You will be using the cryptography classes in the FCL to encrypt and/or decrypt data. In doing so, you want to make sure that no data (e.g., seed values or keys) is left in memory for longer than you are using the cryptography classes. Hackers can sometimes find this information in memory and use it to break your encryption or, worse, to break your encryption, modify the data, and then reencrypt the data and pass it on to your application.

Solution

In order to clear out the key and initialization vector (or seed), you need to call the Clear method on whichever SymmetricAlgorithm- or AsymmetricAlgorithm-derived class you are using. Clear reinitializes the Key and IV properties, preventing them from being found in memory. This is done after saving the key and IV so that you can decrypt later. the section called "Cleaning Up Cryptography Information" encodes a string and then cleans up immediately afterward to provide the smallest window possible for potential attackers.

Example 17-7. Cleaning up cryptography information

using System;
using System.Text;
using System.IO;
using System.Security.Cryptography;

string originalStr = "SuperSecret information";
// Encode data string to be stored in memory.
byte[] originalStrAsBytes = Encoding.ASCII.GetBytes(originalStr);

// Create MemoryStream to contain output.
MemoryStream memStream = new MemoryStream(originalStrAsBytes.Length);
RijndaelManaged rijndael = new RijndaelManaged( );

// Generate secret key and init vector.
rijndael.KeySize = 256;
rijndael.GenerateKey( );
rijndael.GenerateIV( );

// Save the key and IV for later decryption.
byte [] key = rijndael.Key;
byte [] IV = rijndael.IV;

// Create encryptor and stream objects.
ICryptoTransform transform = rijndael.CreateEncryptor(rijndael.Key,
    rijndael.IV);
CryptoStream cryptoStream = new CryptoStream(memStream, transform,
    CryptoStreamMode.Write);

// Write encrypted data to the MemoryStream.
cryptoStream.Write(originalStrAsBytes, 0, originalStrAsBytes.Length);
cryptoStream.FlushFinalBlock( );

// Release all resources as soon as we are done with them
// to prevent retaining any information in memory.
memStream.Close( );
cryptoStream.Close( );
transform.Dispose( );
// This clear statement regens both the key and the init vector so that
// what is left in memory is no longer the values you used to encrypt with.
rijndael.Clear( );

You can also make your life a little easier by taking advantage of the using statement, instead of having to remember to manually call each of the Close methods individually. This code block shows how to use the using statement:

   public static void CleanUpCryptoWithUsing( )
    {
        string originalStr = "SuperSecret information";
        // Encode data string to be stored in memory.
        byte[] originalStrAsBytes = Encoding.ASCII.GetBytes(originalStr);
        byte[] originalBytes = { };

        // Create MemoryStream to contain output.
        using (MemoryStream memStream = new MemoryStream(originalStrAsBytes.Length))
        {
            using (RijndaelManaged rijndael = new RijndaelManaged( ))
            {
                // Generate secret key and init vector.

                rijndael.KeySize = 256;
                rijndael.GenerateKey( );
                rijndael.GenerateIV( );
                // Save off the key and IV for later decryption.
                byte[] key = rijndael.Key;
                byte[] IV = rijndael.IV;

                // Create encryptor and stream objects.
                using (ICryptoTransform transform =
                    rijndael.CreateEncryptor(rijndael.Key, rijndael.IV))
                {
                    using (CryptoStream cryptoStream = new
                           CryptoStream(memStream, transform,
                            CryptoStreamMode.Write))
                    {
                        // Write encrypted data to the MemoryStream.
                        cryptoStream.Write(originalStrAsBytes, 0,
                                originalStrAsBytes.Length);
                        cryptoStream.FlushFinalBlock( );
                    }
                }
            }
        }
    }

Discussion

To make sure your data is safe, you need to close the MemoryStream and CryptoStream objects as soon as possible, as well as calling Dispose on the ICryptoTransform implementation to clear out any resources used in this encryption. The using statement makes this process much easier, makes your code easier to read, and leads to fewer programming mistakes.

See Also

The Algorithm."Symmetric Clear Method" and "AsymmetricAlgorithm.Clear Method" topics in the MSDN documentation.

Verifying That a String Remains Uncorrupted Following Transmission

Problem

You have some text that will be sent across a network to another machine for processing. You need to verify that this message has not been modified in transit.

Solution

Calculate a hash value from the string and append it to the string before it is sent to its destination. Once the destination receives the string, it can remove the hash value and determine whether the string is the same one that was initially sent. It is critical that both sides agree on a hash algorithm that will be used. The SHA-256 algorithm is a good choice and an industry standard.

The CreateStringHash method takes a string as input, adds a hash value to the end of it, and returns the new string, as shown in the section called "Verifying That a String Remains Uncorrupted Following Transmission".

Example 17-8. Verifying that a string remains uncorrupted following transmission

public class HashOps
{
  // The number 44 is the exact length of the base64 representation
  // of the hash value, which was appended to the string.
  private const int HASH_LENGTH = 44;

     public static string CreateStringHash(string unHashedString)
  {
      byte[] encodedUnHashedString = Encoding.Unicode.GetBytes(unHashedString);
      string stringWithHash = ";

      using (SHA256Managed hashingObj = new SHA256Managed( ))
      {
        byte[] hashCode = hashingObj.ComputeHash(encodedUnHashedString);

        string hashBase64 = Convert.ToBase64String(hashCode);
        stringWithHash = unHashedString + hashBase64;
      }

      return (stringWithHash);
  }
     public static bool IsStringCorrupted(string stringWithHash,
                  out string originalStr)
        {
            // Code to quickly test the handling of a tampered string.
            //stringWithHash = stringWithHash.Replace('a', 'b');

            if (stringWithHash.Length <= HASH_LENGTH)
            {
                originalStr = null;
                return (true);
            }

            string hashCodeString =
                stringWithHash.Substring(stringWithHash.Length - HASH_LENGTH);
            string unHashedString =
                stringWithHash.Substring(0, stringWithHash.Length - HASH_LENGTH);

            byte[] hashCode = Convert.FromBase64String(hashCodeString);

            byte[] encodedUnHashedString = Encoding.Unicode.GetBytes(unHashedString);

            bool hasBeenTamperedWith = false;
            using (SHA256Managed hashingObj = new SHA256Managed( ))
            {
              byte[] receivedHashCode = hashingObj.ComputeHash(encodedUnHashedString);
              for (int counter = 0; counter < receivedHashCode.Length; counter+)
              {
                if (receivedHashCode[counter] != hashCode[counter])
                {
                    hasBeenTamperedWith = true;
                    break;
                }
            }

            if (!hasBeenTamperedWith)
            {
              originalStr = unHashedString;
            }
            else
            {
              originalStr = null;
            }
        }

        return (hasBeenTamperedWith);
    }
}

The IsStringCorrupted method is called by the code that receives a string with a hash value appended. This method removes the hash value, calculates a new hash value for the string, and checks to see whether both hash values match. If they match, both strings are exactly the same, and the method returns false. If they don't match, the string has been tampered with, and the method returns true.

Since the CreateStringHash and IsStringCorrepted methods are static members of a class named HashOps, you can call these methods with code like the following:

   public static void VerifyNonStringCorruption( )
    {
        string testString = "This is the string that we'll be testing.";
        string unhashedString;
        string hashedString = HashOps.CreateStringHash(testString);

        bool result = HashOps.IsStringCorrupted(hashedString, out unhashedString);
        Console.WriteLine(result);
        if (!result)
            Console.WriteLine("The string sent is: " + unhashedString);
        else

            Console.WriteLine("The string: " + unhashedString +
                " has become corrupted.");
    }

The output of this method is shown here when the string is uncorrupted:

 False
    The string sent is: This is the string that we'll be testing.

The output of this method is shown here when the string is corrupted:

   False
    The string: This is the string that we'll #$%^(&*2 be testing.
    has become corrupted.

Discussion

You can use a hash, checksum, or cyclic redundancy check (CRC) to calculate a value based on a message. This value is then used at the destination to determine whether the message has been modified during transmission between the source and destination.

This recipe uses a hash value as a reliable method of determining whether a string has been modified. The hash value for this recipe is calculated using the SHA256Managed class. This hash value is 256 bits in size and produces greatly differing results when calculated from strings that are very similar, but not exactly the same. In fact, if a single letter is removed or even capitalized, the resulting hash value will change considerably.

By appending this value to the string, both the string and hash values can be sent to their destination. The destination then removes the hash value and calculates a hash value of its own based on the received string. These two hash values are then compared. If they are equal, the strings are exactly the same. If they are not equal, you can be sure that somewhere between the source and destination, the string was corrupted. This technique is great for verifying that transmission succeeded without errors, but it does not guarantee against malicious tampering. To protect against malicious tampering, use an asymmetric algorithm: sign the string with a private key and verify the signature with a public key.

The CreateStringHash method first converts the unhashed string into a byte array using the GetBytes method of the UnicodeEncoding class. This byte array is then passed to the ComputeHash method of the SHA256Managed class.

Once the hash value is calculated, the byte array containing the hash code is converted to a string containing base64 digits, using the Convert.ToBase64String method. This method accepts a byte array, converts it to a string of base64 digits, and returns that string. The reason for doing this is to convert all unsigned integers in the byte array to values that can be represented in a string data type. The last thing that this method does is to append the hash value to the end of the string and return the newly hashed string.

The IsStringCorrupted method accepts a hashed string and an out parameter that will return the unhashed string. This method returns a Boolean; as previously mentioned, true indicates that the string has been modified, false indicates that the string is unmodified.

This method first removes the hash value from the end of the StringWithHash variable. Next, a new hash is calculated using the string portion of the StringWithHash variable. These two hash values are compared. If they are the same, the string has been received, unmodified. Note that if you change the hashing algorithm used, you must change it in both this method and the CreateStringHash method. You must also change the HASH_LENGTH constant in the IsStringCorrupted method to an appropriate size for the new hashing algorithm. This number is the exact length of the base64 representation of the hash value, which was appended to the string.

See Also

The "SHA256Managed Class," "Convert.ToBase64String Method," and "Convert. FromBase64String Method" topics in the MSDN documentation.

Storing Data Securely

Problem

You need to store settings data about individual users for use by your application and keep this data isolated from other instances of your application run by different users.

Solution

You can use isolated storage to establish per user data stores for your application data and then use hashed values for critical data.

To illustrate how to do this for settings data, you create the following UserSettings class. UserSettings holds only two pieces of information: the user identity (current WindowsIdentity) and the password for your application. The user identity is accessed via the User property, and the password is accessed via the Password property. Note that the password field is created the first time and is stored as a salted hashed value to keep it secure. The combination of the isolated storage and the hashing of the password value helps to strengthen the security of the password by using the defense in depth principle. Salting the hash is an extra measure of protection that not only protects the password against dictionary type attacks, but it also prevents an attacker from easily determining if two users have the same password by comparing the hashes.

The settings data is held in XML that is stored in the isolated storage scope and accessed via an XmlDocument instance.

This solution uses the following namespaces:

  using System;
    using System.IO;
    using System.IO.IsolatedStorage;
    using System.Xml;
    using System.Text;
    using System.Diagnostics;
    using System.Security.Principal;
    using System.Security.Cryptography;

The UserSettings class is shown in Example 17-9, "UserSettings class".

Example 17-9. UserSettings class

// Class to hold user settings
public class UserSettings
{
    isoFileStream = null;
    XmlDocument settingsDoc = null;
    const string storageName = "SettingsStorage.xml";

    // Constructor
    public UserSettings(string password)
    {
        // Get the isolated storage.
        using (IsolatedStorageFile isoStorageFile =
              IsolatedStorageFile.GetUserStoreForDomain( ))
        {
            // Create an internal DOM for settings.
            settingsDoc = new XmlDocument( );
            // If no settings, create default.
            if(isoStorageFile.GetFileNames(storageName).Length == 0)
            {
                using (IsolatedStorageFileStream isoFileStream =
                     new IsolatedStorageFileStream(storageName,
                                               FileMode.Create,
                                               isoStorageFile))
                {
                    using (XmlTextWriter writer = new
                          XmlTextWriter(isoFileStream,Encoding.UTF8))
                    {
                        writer.WriteStartDocument( );
                        writer.WriteStartElement("Settings");
                        writer.WriteStartElement("User");
                        // Get current user.
                        WindowsIdentity user = WindowsIdentity.GetCurrent(
                        writer.WriteString(user.Name);
                        writer.WriteEndElement( );
                        writer.WriteStartElement("Password");

                        // Pass null to CreateHashedPassword as the salt
                        // to establish one
                        // CreateHashedPassword appears shortly
                        string hashedPassword =
                                CreateHashedPassword(password,null);
                        writer.WriteString(hashedPassword);
                        writer.WriteEndElement( );
                        writer.WriteEndElement( );
                        writer.WriteEndDocument( );
                        Console.WriteLine("Creating settings for " + user.Name);
                    }
                }
            }

            // Set up access to settings store.
            using (IsolatedStorageFileStream isoFileStream =
                 new IsolatedStorageFileStream(storageName,
                                           FileMode.Open,
                                           isoStorageFile))
            {
                // Load settings from isolated filestream
                settingsDoc.Load(isoFileStream);
                Console.WriteLine("Loaded settings for " + User);
            }
        }
    }

The User property provides access to the WindowsIdentity of the user that this set of settings belongs to:

  // User property
    public string User
    {
        get
        {
            XmlNode userNode = settingsDoc.SelectSingleNode("Settings/User");
            if(userNode != null)
            {
                return userNode.InnerText;
            }
            return ";
        }
    }

The Password property gets the salted and hashed password value from the XML store and, when updating the password, takes the plain text of the password and creates the salted and hashed version, which is then stored:

   // Password property
    public string Password
    {
        get
        {
            XmlNode pwdNode =
                      settingsDoc.SelectSingleNode("Settings/Password");
            if(pwdNode != null)

            {
                return pwdNode.InnerText;
            }
            return ";
        }
        set
        {
            XmlNode pwdNode =
                      settingsDoc.SelectSingleNode("Settings/Password");

            string hashedPassword = CreateHashedPassword(value,null);
            if(pwdNode != null)
            {
                pwdNode.InnerText = hashedPassword;
            }
            else
            {  
                XmlNode settingsNode =
                          settingsDoc.SelectSingleNode("Settings");
                XmlElement pwdElem =
                             settingsDoc.CreateElement("Password");
                pwdElem.InnerText=hashedPassword;
                settingsNode.AppendChild(pwdElem);
            }
        }
    }

The CreateHashedPassword method creates the salted and hashed password. The password parameter is the plain text of the password; the existingSalt parameter is the salt to use when creating the salted and hashed version. If no salt exists, such as the first time a password is stored, existingSalt should be passed as null, and a random salt will be generated.

Once you have the salt, it is combined with the plain text password and hashed using the SHA512Managed class. The salt value is then appended to the end of the hashed value and returned. The salt is appended so that when you attempt to validate the password, you know what salt was used to create the hashed value. The entire value is then base64-encoded and returned:

 // Make a hashed password.
    private string CreateHashedPassword(string password,
                                        byte[] existingSalt)
    {
        byte [] salt = null;
        if(existingSalt == null)
        {
            // Make a salt of random size.
            // Create a stronger hash code using RNGCryptoServiceProvider.
            byte[] random = new byte[1];
            RNGCryptoServiceProvider rngSize = new RNGCryptoServiceProvider( );
            // Populate with random bytes.
            rngSize.GetBytes(random);
            // Convert random bytes to string.

            int size = Convert.ToInt32(random);

            // Create salt array.
            salt = new byte[size];

            // Use the better random number generator to get
            // bytes for the salt.
            RNGCryptoServiceProvider rngSalt =
                new RNGCryptoServiceProvider( );
            rngSalt.GetNonZeroBytes(salt);
        }
        else
            salt = existingSalt;

            // Turn string into bytes.
            byte[] pwd = Encoding.UTF8.GetBytes(password);

            // Make storage for both password and salt.
            byte[] saltedPwd = new byte[pwd.Length + salt.Length];

            // Add pwd bytes first.
            pwd.CopyTo(saltedPwd,0);
            // now add salt
            salt.CopyTo(saltedPwd,pwd.Length);

            // Use SHA512 as the hashing algorithm.
            byte[] hashWithSalt = null;
            using (SHA512Managed sha512 = new SHA512Managed( ))
            {
                // Get hash of salted password.
                byte[] hash = sha512.ComputeHash(saltedPwd);

                // Append salt to hash so we have it.
                hashWithSalt = new byte[hash.Length + salt.Length];

                // Copy in bytes.
                hash.CopyTo(hashWithSalt,0);
                salt.CopyTo(hashWithSalt,hash.Length);
            }

            // Return base64-encoded hash with salt.
            return Convert.ToBase64String(hashWithSalt);
        }

To check a given password against the stored value (which is salted and hashed), you call IsPasswordValid and pass in the plain text password to check. First, the stored value is retrieved using the Password property and converted from base64. Since you know you used SHA512, there are 512 bits in the hash. But you need the byte size, so you do the math and get that size in bytes. This allows you to figure out where to get the salt from in the value, so you copy it out of the value and call CreateHashedPassword using that salt and the plain text password parameter. This gives you the hashed value for the password that was passed in to verify. Once you have that, you just compare it to the Password property to see whether you have a match and return true or false as appropriate:

     // Check the password against our storage.
        public bool IsPasswordValid(string password)
        {
            // Get bytes for password.
            // This is the hash of the salted password and the salt.
            byte[] hashWithSalt = Convert.FromBase64String(Password);

            // We used 512 bits as the hash size (SHA512).
            int hashSizeInBytes = 512 / 8;

            // Make holder for original salt.
            int saltSize = hashWithSalt.Length - hashSizeInBytes;
            byte[] salt = new byte[saltSize];

            // Copy out the salt.
            Array.Copy(hashWithSalt,hashSizeInBytes,salt,0,saltSize);

            // Figure out hash for this password.
            string passwordHash = CreateHashedPassword(password,salt);

            // If the computed hash matches the specified hash,
            // the plain text value must be correct.
            // See if Password (stored) matched password passed in.
            return (Password == passwordHash);
        }
    }

Code that uses the UserSettings class is shown here:

    class IsoApplication
    {
      static void Main(string[] args)
      {
           if(args.Length > 0)
           {
               UserSettings settings = new UserSettings(args[0]);
               if(settings.IsPasswordValid(args[0]))
                {
                   Console.WriteLine("Welcome");
                   return;
                }
           }
           Console.WriteLine("The system could not validate your credentials");
        }
    }

The way to use this application is to pass the password on the command line as the first argument. This password is then checked against the UserSettings, which is stored in the isolated storage for this particular user. If the password is correct, the user is welcomed; if not, the user is shown the door.

Discussion

Isolated storage allows an application to store data that is unique to the application and the user running it. This storage allows the application to write out state information that is not visible to other applications or even other users of the same application. Isolated storage is based on the code identity as determined by the CLR, and it stores the information either directly on the client machine or in isolated stores that can be opened and roam with the user. The storage space available to the application is directly controllable by the administrator of the machine on which the application operates.

The Solution uses isolation by User, AppDomain, and Assembly by calling IsolatedStorageFile.GetUserStoreForDomain. This creates an isolated store that is accessible by only this user in the current assembly in the current AppDomain:

   // Get the isolated storage.
    isoStorageFile = IsolatedStorageFile.GetUserStoreForDomain( );

The Storeadm.exe utility will allow you to see which isolated-storage stores have been set up on the machine by running the utility with the /LIST command-line switch. Storeadm.exe is part of the .NET Framework SDK and can be located in your Visual Studio installation directory under the \SDK\v2.0\Bin subdirectory.

The output after using the UserSettings class would look like this:

  C:\>storeadm /LIST
    Microsoft (R) .NET Framework Store Admin 1.1.4322.573
    Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.

    Record #1
    [Domain]
    <System.Security.Policy.Url version="1">
       <Url>file://D:/PRJ32/Book/IsolatedStorage/bin/Debug/IsolatedStorage.exe</Url>

    </System.Security.Policy.Url>

    [Assembly]
    <System.Security.Policy.Url version="1">
       <Url>file://D:/PRJ32/Book/IsolatedStorage/bin/Debug/IsolatedStorage.exe</Url>

    </System.Security.Policy.Url>

               Size : 1024

Passwords should never be stored in plain text, period. It is a bad habit to get into, so in the UserSettings class, you have added the salting and hashing of the password value via the CreateHashedPassword method and verification through the IsPasswordValid method. Adding a salt to the hash helps to strengthen the protection on the value being hashed so that the isolated storage, the hash, and the salt now protect the password you are storing.

See Also

The "IsolatedStorageFile Class," "IsolatedStorageStream Class," "About Isolated Storage," and "ComputeHash Method" topics in the MSDN documentation.

Making a Security Assert Safe

Problem

You want to assert that at a particular point in the call stack, a given permission is available for all subsequent calls. However, doing this can easily open a security hole to allow other malicious code to spoof your code or to create a back door into your component. You want to assert a given security permission, but you want to do so in a secure and efficient manner.

Solution

In order to make this approach secure, you need to call Demand on the permissions that the subsequent calls need. This makes sure that code that doesn't have these permissions can't slip by due to the Assert. The Demand is done to ensure that you have indeed been granted this permission before using the Assert to short-circuit the stackwalk. This is demonstrated by the function CallSecureFunctionSafelyAndEfficiently, which performs a Demand and an Assert before calling SecureFunction, which in turn does a Demand for a ReflectionPermission.

The code listing for CallSecureFunctionSafelyAndEfficiently is shown in Example 17-10, "CallSecureFunctionSafelyAndEfficiently function".

Example 17-10. CallSecureFunctionSafelyAndEfficiently function

public static void CallSecureFunctionSafelyAndEfficiently( )
{

    // Set up a permission to be able to access nonpublic members
    // via reflection.
    ReflectionPermission perm =
        new ReflectionPermission(ReflectionPermissionFlag.MemberAccess);

    // Demand the permission set we have compiled before using Assert
    // to make sure we have the right before we Assert it. We do
    // the Demand to ensure that we have checked for this permission
    // before using Assert to short-circuit stackwalking for it, which
    // helps us stay secure, while performing better.
    perm.Demand( );

    // Assert this right before calling into the function that
    // would also perform the Demand to short-circuit the stack walk
    // each call would generate. The Assert helps us to optimize
    // our use of SecureFunction.

    perm.Assert( );
    // We call the secure function 100 times but only generate
    // the stackwalk from the function to this calling function
    // instead of walking the whole stack 100 times.
    for(int i=0;i<100;i+)
    {
        SecureFunction( );
    }
}

The code listing for SecureFunction is shown here:

  public static void SecureFunction( )
    {
        // Set up a permission to be able to access nonpublic members
        // via reflection.
        ReflectionPermission perm =
            new ReflectionPermission(ReflectionPermissionFlag.MemberAccess);

        // Demand the right to do this and cause a stackwalk.
        perm.Demand( );

        // Perform the action here...
    }

Discussion

In the demonstration function CallSecureFunctionSafelyAndEfficiently, the function you are calling (SecureFunction) performs a Demand on a ReflectionPermission to ensure that the code can access nonpublic members of classes via reflection. Normally, this would result in a stackwalk for every call to SecureFunction. The Demand in CallSecureFunctionSafelyAndEfficiently is there only to protect against the usage of the Assert in the first place. To make this more efficient, you can use Assert to state that all functions issuing Demands that are called from this one do not have to stackwalk any further. The Assert says stop checking for this permission in the call stack. In order to do this, you need the permission to call Assert.

The problem comes in with this Assert, as it opens up a potential luring attack where SecureFunction is called via CallSecureFunctionSafelyAndEfficiently, which calls Assert to stop the Demand stackwalks from SecureFunction. If unauthorized code without ReflectionPermission were able to call CallSecureFunctionSafelyAndEfficiently, the Assert would prevent the SecureFunction Demand call from determining that there is some code in the call stack without the proper rights. This is the power of the call stack checking in the CLR when a Demand occurs.

In order to protect against this, you issue a Demand for the ReflectionPermission needed by SecureFunction in CallSecureFunctionSafelyAndEfficiently to close this hole before issuing the Assert. The combination of this Demand and the Assert causes you to do one stackwalk instead of the original 100 that would have been caused by the Demand in SecureFunction.

Security optimization techniques, such as using Assert in this case (even though it isn't the primary reason to use Assert), can help class library as well as control developers who are trusted to perform Asserts in order to speed the interaction of their code with the CLR; but if used improperly, these techniques can also open up holes in the security picture. This example shows that you can have both performance and security where secure access is concerned.

If you are using Assert, be mindful that stackwalk overrides should never be made in a class constructor. Constructors are not guaranteed to have any particular security context, nor are they guaranteed to execute at a specific point in time. This lack leads to the call stack not being well defined, and Assert used here can produce unexpected results.

One other thing to remember with Assert is that you can have only one active Assert in a function at a given time. If you Assert the same permission twice, a SecurityException is thrown by the CLR. You must revert the original Assert first using RevertAssert. Then, you can declare the second Assert.

See Also

The "CodeAccessSecurity.Assert Method," "CodeAccessSecurity.Demand Method," "CodeAccessSecurity.RevertAssert Method," and "Overriding Security Checks" topics in the MSDN documentation.

Verifying That an Assembly Has Been Granted Specific Permissions

Problem

When your assembly requests optional permissions (such as asking for disk access to enable users to export data to disk as a product feature) using the SecurityAction.RequestOptional flag, it might or might not get those permissions. Regardless, your assembly will still load and execute. You need a way to verify whether your assembly actually obtained those permissions. This can help prevent many security exceptions from being thrown. For example, if you optionally requested read/write permissions on the registry but did not receive them, you could disable the user interface controls that are used to read and store application settings in the registry.

Solution

Check to see if your assembly received the optional permissions using the SecurityManager.IsGranted method like this:

 using System;
    using System.Text.RegularExpressions;
    using System.Web;
    using System.Net;
    using System.Security;

    Regex regex = new Regex(@"http://www\.oreilly\.com/.*");
    WebPermission webConnectPerm = new WebPermission(NetworkAccess. Connect,regex);
    if(SecurityManager.IsGranted(webConnectPerm))
    {
       // Connect to the O'Reilly site.
    }

This code sets up a Regex for the O'Reilly website and then uses it to create a WebPermission for connecting to that site and all sites containing the string. You then check the WebPermission by calling SecurityManager.IsGranted to see whether you have permission to do this.

Discussion

The IsGranted method is a lightweight way of determining whether permission is granted for an assembly without first incurring the full stackwalk that a Demand gives you. Note, however, that once you exercise the code that performs the Demand, the full stackwalk will then take place. The drawback to this approach is that the code is still subject to a luring attack if Assert is misused, so you need to consider where the call to IsGranted is being made in the overall scheme of your security.

Some of the reasons you might design an assembly to have optional permissions is for deployment in different customer scenarios. In some scenarios (such as desktop applications), it might be acceptable to have an assembly that can perform more robust actions (talk to a database, create network traffic via HTTP, etc.). In other scenarios, you can defer these actions if the customer does not wish to grant enough permissions for these extra services to function.

See Also

The "WebPermission Class," "SecurityManager Class," and "IsGranted Method" topics in the MSDN documentation.

Minimizing the Attack Surface of an Assembly

Problem

Someone attacking your assembly will first attempt to find out as many things as possible about your assembly and then use this information in constructing the attack(s). The more surface area you give to attackers, the more they have to work with. You need to minimize what your assembly is allowed to do so that, if an attacker is successful in taking it over, the attacker will not have the necessary privileges to do any damage to the system.

Solution

Use the SecurityAction.RequestRefuse enumeration member to indicate, at an assembly level, the permissions that you do not wish this assembly to have. This will force the CLR to refuse these permissions to your code and will ensure that, even if another part of the system is compromised, your code cannot be used to perform functions that it does not need the rights to do.

The following example allows the assembly to perform file I/O as part of its minimal permission set but explicitly refuses to allow this assembly to have permissions to skip verification:

 [assembly: FileIOPermission(SecurityAction.RequestMinimum,Unrestricted=true)]
    [assembly: SecurityPermission(SecurityAction.RequestRefuse,
                 SkipVerification=false)]

Discussion

Once you have determined what permissions your assembly needs as part of your normal security testing, you can use RequestRefuse to lock down your code. If this seems extreme, think of scenarios in which your code could be accessing a data store containing sensitive information, such as social security numbers or salary information. This proactive step can help you show your customers that you take security seriously and can help defend your interests in case a break-in occurs on a system that your code is part of.

One serious consideration with this approach is that the use of RequestRefuse marks your assembly as partially trusted. This in turn prevents it from calling any strongnamed assembly that hasn't been marked with the AllowPartiallyTrustedCallers attribute.

See Also

Chapter 8, Diagnostics of Microsoft Patterns & Practices Group: https://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnnetsec/html/THCMCh08.asp; see the "SecurityAction Enumeration" and "Global Attributes" topics in the MSDN documentation.

Obtaining Security/Audit Information

Problem

You need to obtain the security rights and/or audit information for a file or registry key.

Solution

When obtaining security/audit information for a file, use the static GetAccessControl method of the File class to obtain a System.Security.AccessControl.FileSecurity object. Use the FileSecurity object to access the security and audit information for the file. These steps are demonstrated in Example 17-11, "Obtaining security audit information".

Example 17-11. Obtaining security audit information

public static void ViewFileRights()
{
    // Get security information from a file.
    string file = @"c:\FOO.TXT";
    FileSecurity fileSec = File.GetAccessControl(file);
    DisplayFileSecurityInfo(fileSec);
}

public static void DisplayFileSecurityInfo(FileSecurity fileSec)
{
    Console.WriteLine("GetSecurityDescriptorSddlForm: {0}",
        fileSec.GetSecurityDescriptorSddlForm(AccessControlSections.All));

    foreach (FileSystemAccessRule ace in
            fileSec.GetAccessRules(true, true, typeof(NTAccount)))
    {
        Console.WriteLine("\tIdentityReference.Value: {0}",
                          ace.IdentityReference.Value);
        Console.WriteLine("\tAccessControlType: {0}", ace.AccessControlType);
        Console.WriteLine("\tFileSystemRights: {0}", ace.FileSystemRights);
        Console.WriteLine("\tInheritanceFlags: {0}", ace.InheritanceFlags);
        Console.WriteLine("\tIsInherited: {0}", ace.IsInherited);
        Console.WriteLine("\tPropagationFlags: {0}", ace.PropagationFlags);

        Console.WriteLine("-----------------\r\n\r\n");
    }

    foreach (FileSystemAuditRule ace in
            fileSec.GetAuditRules(true, true, typeof(NTAccount)))
    {
        Console.WriteLine("\tIdentityReference.Value: {0}",
                          ace.IdentityReference.Value);
        Console.WriteLine("\tAuditFlags: {0}", ace.AuditFlags);
        Console.WriteLine("\tFileSystemRights: {0}", ace.FileSystemRights);
        Console.WriteLine("\tInheritanceFlags: {0}", ace.InheritanceFlags);
        Console.WriteLine("\tIsInherited: {0}", ace.IsInherited);
        Console.WriteLine("\tPropagationFlags: {0}", ace.PropagationFlags);

        Console.WriteLine("-----------------\r\n\r\n");
    }

    Console.WriteLine("GetGroup(typeof(NTAccount)).Value: {0}",
                      fileSec.GetGroup(typeof(NTAccount)).Value);
    Console.WriteLine("GetOwner(typeof(NTAccount)).Value: {0}",
                      fileSec.GetOwner(typeof(NTAccount)).Value);

    Console.WriteLine("---------------------------------------\r\n\r\n\r\n");
}

These methods produce the following output:

 GetSecurityDescriptorSddlForm: O:BAG:SYD:PAI(A;;FA;;;SY)(A;;FA;;;BA)
        IdentityReference.Value: NT AUTHORITY\SYSTEM
        AccessControlType: Allow
        FileSystemRights: FullControl
        InheritanceFlags: None
        IsInherited: False
        PropagationFlags: None
    ----------------

        IdentityReference.Value: BUILTIN\Administrators
        AccessControlType: Allow
        FileSystemRights: FullControl
        InheritanceFlags: None
        IsInherited: False
        PropagationFlags: None
    ----------------

    GetGroup(typeof(NTAccount)).Value: NT AUTHORITY\SYSTEM
    GetOwner(typeof(NTAccount)).Value: BUILTIN\Administrators

When obtaining security/audit information for a registry key, use the GetAccessControl instance method of the Microsoft.Win32.RegistryKey class to obtain a System. Security.AccessControl.RegistrySecurity object. Use the RegistrySecurity object to access the security and audit information for the registry key. These steps are demonstrated in Example 17-12, "Getting security or audit information for a registry key".

Example 17-12. Getting security or audit information for a registry key

public static void ViewRegKeyRights()
{
    // Get security information from a registry key.
    using (RegistryKey regKey =
        Registry.LocalMachine.OpenSubKey(@"SOFTWARE\MyCompany\MyApp"))
    {
        RegistrySecurity regSecurity = regKey.GetAccessControl();
        DisplayRegKeySecurityInfo(regSecurity);
    }
}

public static void DisplayRegKeySecurityInfo(RegistrySecurity regSec)
{
    Console.WriteLine("GetSecurityDescriptorSddlForm: {0}",
        regSec.GetSecurityDescriptorSddlForm(AccessControlSections.All));

    foreach (RegistryAccessRule ace in
            regSec.GetAccessRules(true, true, typeof(NTAccount)))
    {
        Console.WriteLine("\tIdentityReference.Value: {0}",
                          ace.IdentityReference.Value);
        Console.WriteLine("\tAccessControlType: {0}", ace.AccessControlType);
        Console.WriteLine("\tRegistryRights: {0}", ace.RegistryRights.ToString());
        Console.WriteLine("\tInheritanceFlags: {0}", ace.InheritanceFlags);
        Console.WriteLine("\tIsInherited: {0}", ace.IsInherited);
        Console.WriteLine("\tPropagationFlags: {0}", ace.PropagationFlags);

        Console.WriteLine("-----------------\r\n\r\n");
    }

    foreach (RegistryAuditRule ace in
            regSec.GetAuditRules(true, true, typeof(NTAccount)))
    {
        Console.WriteLine("\tIdentityReference.Value: {0}",
                          ace.IdentityReference.Value);
        Console.WriteLine("\tAuditFlags: {0}", ace.AuditFlags);
        Console.WriteLine("\tRegistryRights: {0}", ace.RegistryRights.ToString());
        Console.WriteLine("\tInheritanceFlags: {0}", ace.InheritanceFlags);
        Console.WriteLine("\tIsInherited: {0}", ace.IsInherited);
        Console.WriteLine("\tPropagationFlags: {0}", ace.PropagationFlags);

        Console.WriteLine("-----------------\r\n\r\n");
    }
    Console.WriteLine("GetGroup(typeof(NTAccount)).Value: {0}",
                      regSec.GetGroup(typeof(NTAccount)).Value);
    Console.WriteLine("GetOwner(typeof(NTAccount)).Value: {0}",
                      regSec.GetOwner(typeof(NTAccount)).Value);

    Console.WriteLine("---------------------------------------\r\n\r\n\r\n");
}

These methods produce the following output:

 GetSecurityDescriptorSddlForm: O:S-1-5-21-329068152-1383384898-682003330-1004G:S-
    15-21-329068152-1383384898-682003330-513D:

    AI(A;ID;KR;;;BU)(A;CIIOID;GR;;;BU)(A;ID;KA;;;BA)(A;CIIOID;GA;;;BA)(A;ID;KA;;;SY)(A;CI
            IOID;GA;;;SY)(A;ID;KA;;;S-1-5-21-329068152-1383384898-682003330-
            1004)(A;CIIOID;GA;;;CO)
                IdentityReference.Value: BUILTIN\Users
                AccessControlType: Allow
                RegistryRights: ReadKey
                InheritanceFlags: None
                IsInherited: True
                PropagationFlags: None
            -----------------

                IdentityReference.Value: BUILTIN\Users
                AccessControlType: Allow
                RegistryRights: -2147483648
                InheritanceFlags: ContainerInherit
                IsInherited: True
                PropagationFlags: InheritOnly
            -----------------

                IdentityReference.Value: BUILTIN\Administrators
                AccessControlType: Allow
                RegistryRights: FullControl
                InheritanceFlags: None
                IsInherited: True
                PropagationFlags: None
            -----------------

                IdentityReference.Value: BUILTIN\Administrators
                AccessControlType: Allow
                RegistryRights: 268435456
                InheritanceFlags: ContainerInherit
                IsInherited: True
                PropagationFlags: InheritOnly
            -----------------

                IdentityReference.Value: NT AUTHORITY\SYSTEM
                AccessControlType: Allow
                RegistryRights: FullControl
                InheritanceFlags: None
                IsInherited: True
                PropagationFlags: None
            -----------------

                IdentityReference.Value: NT AUTHORITY\SYSTEM
                AccessControlType: Allow
                RegistryRights: 268435456
                InheritanceFlags: ContainerInherit
                IsInherited: True
                PropagationFlags: InheritOnly
            -----------------

                IdentityReference.Value: OPERATOR-C1EFE0\Admin
                AccessControlType: Allow
                RegistryRights: FullControl
                InheritanceFlags: None
                IsInherited: True
                PropagationFlags: None
            -----------------

                IdentityReference.Value: CREATOR OWNER
                AccessControlType: Allow
                RegistryRights: 268435456
                InheritanceFlags: ContainerInherit
                IsInherited: True
                PropagationFlags: InheritOnly
            -----------------

            GetGroup(typeof(NTAccount)).Value: OPERATOR-C1EFE0\None
            GetOwner(typeof(NTAccount)).Value: OPERATOR-C1EFE0\Admin
            ---------------------------------------

Discussion

The essential method that is used to obtain the security information for a file or registry key is the GetAccessControl method. When this method is called on the RegistryKey object, a RegistrySecurity object is returned. However, when this method is called on a File class, a FileSecurity object is returned. The RegistrySecurity and FileSecurity objects essentially represent a Discretionary Access Control List (DACL), which is what developers writing code in unmanaged languages such as C++ are used to working with.

The RegistrySecurity and FileSecurity objects each contain a list of security rules that has been applied to the system object that it represents. The RegistrySecurity object contains a list of RegistryAccessRule objects, and the FileSecurity object contains a list of FileSystemAccessRule objects. These rule objects are the equivalent of the Access Control Entries (ACE) that make up the list of security rules within a DACL.

System objects other than just the File class and RegistryKey object allow security privileges to be queried. Table 17.1, "List of all *Security and *AccessRule objects and the types to which they apply" lists all the .NET Framework classes that return a security object type and what that type is. In addition, the rule-object type that is contained in the security object is also listed.

Table 17.1. List of all *Security and *AccessRule objects and the types to which they apply

Class

Object returned by the GetAccessControl method

Rule-object type contained within the security object

Directory

DirectorySecurity

FileSystemAccessRule

DirectoryInfo

DirectorySecurity

FileSystemAccessRule

EventWaitHandle

EventWaitHandleSecurity

EventWaitHandleAccessRule

File

FileSecurity

FileSystemAccessRule

FileInfo

FileSecurity

FileSystemAccessRule

FileStream

FileSecurity

FileSystemAccessRule

Mutex

MutexSecurity

MutexAccessRule

RegistryKey

RegistrySecurity

RegistryAccessRule

Semaphore

SemaphoreSecurity

SemaphoreAccessRule

The abstraction of a system object's DACL through the *Security objects and the abstraction of a DACL's ACE through the *AccessRule objects allows easy access to the security privileges of that system object. In previous versions of the .NET Framework, these DACLs and their ACEs would have been accessible only in unmanaged code. With the .NET 2.0 Framework and later, you now have access to view and program these objects.

See Also

the section called "Granting/Revoking Access to a File or Registry Key"; the "System.IO.File.GetAccessControl Method," "System.Security. AccessControl.FileSecurity Class," "Microsoft.Win32.RegistryKey.GetAccessControl Method," and "System.Security.AccessControl.RegistrySecurity Class" topics in the MSDN documentation.

Granting/Revoking Access to a File or Registry Key

Problem

You need to change the security privileges of either a file or registry key programmatically.

Solution

The code shown in Example 17-13, "Granting and revoking the right to perform write actions on a registry key" grants and then revokes the ability to perform write actions on a registry key.

Example 17-13. Granting and revoking the right to perform write actions on a registry key

public static void GrantRevokeRegKeyRights()
{
    NTAccount user = new NTAccount(@"WRKSTN\ST");

    using (RegistryKey regKey = Registry.LocalMachine.OpenSubKey(
                            @"SOFTWARE\MyCompany\MyApp"))
    {
        GrantRegKeyRights(regKey, user, RegistryRights.WriteKey,
           InheritanceFlags.None, PropagationFlags.None, AccessControlType.Allow);
        RevokeRegKeyRights(regKey, user, RegistryRights.WriteKey,
                       InheritanceFlags.None, PropagationFlags.None,
                       AccessControlType.Allow)
    }
}

public static void GrantRegKeyRights(RegistryKey regKey,
                                     NTAccount user,
                                     RegistryRights rightsFlags,
                                     InheritanceFlags inherFlags,
                                     PropagationFlags propFlags,
                                     AccessControlType actFlags)
{
    Registry Security regSecurity = regKey.GetAccessControl();

    RegistryAccessRule rule = new RegistryAccessRule(user, rightsFlags, inherFlags,
                                                     propFlags, actFlags);
    regSecurity.AddAccessRule(rule);
    regKey.SetAccessControl(regSecurity);
}

public static void RevokeRegKeyRights(RegistryKey regKey,
                                      NTAccount user,
                                      RegistryRights rightsFlags,
                                      InheritanceFlags inherFlags,
                                      PropagationFlags propFlags,
                                      AccessControlType actFlags)
{
    RegistrySecurity regSecurity = regKey.GetAccessControl();

    RegistryAccessRule rule = new RegistryAccessRule(user, rightsFlags, inherFlags,
                                                     propFlags, actFlags);
    regSecurity.RemoveAccessRuleSpecific(rule);

    regKey.SetAccessControl(regSecurity);
}

The code shown in Example 17-14, "Granting and revoking the right to delete a file" grants and then revokes the ability to delete a file.

Example 17-14. Granting and revoking the right to delete a file

public static void GrantRevokeFileRights()
{
    NTAccount user = new NTAccount(@"WRKSTN\ST");

    string file = @"c:\FOO.TXT";
    GrantFileRights(file, user, FileSystemRights.Delete, InheritanceFlags.None,
                    PropagationFlags.None, AccessControlType.Allow);
    RevokeFileRights(file, user, FileSystemRights.Delete, InheritanceFlags.None,
                     PropagationFlags.None, AccessControlType.Allow);
}

public static void GrantFileRights(string file,
                                   NTAccount user,
                                   FileSystemRights rightsFlags,
                                   InheritanceFlags inherFlags,
                                   PropagationFlags propFlags,
                                   AccessControlType actFlags)
{
    FileSecurity fileSecurity = File.GetAccessControl(file);
    FileSystemAccessRule rule = new FileSystem AccessRule(user, rightsFlags,
                                                         inherFlags, propFlags,
                                                         actFlags);
    fileSecurity.AddAccessRule(rule);
    File.SetAccessControl(file, fileSecurity);
}

public static void RevokeFileRights(string file,
                                    NTAccount user,
                                    FileSystemRights rightsFlags,
                                    InheritanceFlags inherFlags,
                                    PropagationFlags propFlags,
                                    AccessControlType actFlags)
{
    FileSecurity fileSecurity = File.GetAccessControl(file);

    FileSystemAccessRule rule = new FileSystemAccessRule(user, rightsFlags,
                                                         inherFlags, propFlags,
                                                         actFlags);
    fileSecurity.RemoveAccessRuleSpecific(rule);
    File.SetAccessControl(file, fileSecurity);
}

Discussion

When granting or revoking access rights on a file or registry key, you need two things. The first is a valid NTAccount object. This object essentially encapsulates a user or group account. A valid NTAccount object is required in order to create either a new RegistryAccessRule or a new FileSystemAccessRule. The NTAccount identifies the user or group this access rule will apply to. Note that the string passed in to the NTAccount constructor must be changed to a valid user or group name that exists on your machine. If you pass in the name of an existing user or group account that has been disabled, an IdentityNotMappedException will be thrown with the message "Some or all identity references could not be translated."

The second item that is needed is either a valid RegistryKey object, if you are modifying security access to a registry key, or a string containing a valid path and filename to an existing file. These objects will have security permissions either granted to them or revoked from them.

Once these two items have been obtained, you can use the second item to obtain a security object, which contains the list of access-rule objects. For example, the following code obtains the security object for the registry key HKEY-LOCAL_ MACHINE\SOFTWARE\MyCompany\MyApp:

    RegistryKey regKey = Registry.LocalMachine.OpenSubKey(
                                @"SOFTWARE\MyCompany\MyApp");
    RegistrySecurity regSecurity = regKey.GetAccessControl();

The following code obtains the security object for the FOO.TXT file:

   string file = @"c:\FOO.TXT";
    FileSecurity fileSecurity = File.Get AccessControl(file);

Now that you have your particular security object, you can create an access-rule object that will be added to this security object. To do this, you need to create a new access rule. For a registry key, you have to create a new RegistryAccessRule object, and for a file, you have to create a new FileSystemAccessRule object. To add this access rule to the correct security object, you call the SetAccessControl method on the security object. Note that RegistryAccessRule objects can be added only to RegistrySecurity objects, and FileSystemAccessRule objects can be added only to FileSecurity objects.

To remove an access-rule object from a system object, you follow the same set of steps, except that you call the RemoveAccessRuleSpecific method instead of AddAccessRule. RemoveAccessRuleSpecific accepts an access-rule object and attempts to remove the rule that exactly matches this rule object from the security object. As always, you must remember to call the SetAccessControl method to apply any changes to the actual system object.

For a list of other classes that allow security permissions to be modified programmatically, see the section called "Protecting String Data with Secure Strings".

See Also

the section called "Obtaining Security/Audit Information"; the "System.IO.File.GetAccessControl Method," "System.Security. AccessControl.FileSecurity Class," "System.Security.AccessControl.FileSystemAc-cessRule Class," "Microsoft.Win32.RegistryKey.GetAccessControl Method," "System.Security.AccessControl.RegistrySecurity Class," and "System.Security. AccessControl.RegistryAccessRule Class" topics in the MSDN documentation.

Protecting String Data with Secure Strings

Problem

You need to store sensitive information, such as a social security number, in a string. However, you do not want prying eyes to be able to view this data in memory.

Solution

Use the SecureString object. To place text from a stream object within a SecureString object, use the following method:

    public static SecureString CreateSecureString(StreamReader secretStream)
    {
        SecureString secretStr = new SecureString();
        char buf;
        while (secretStream.Peek() >= 0)
        {
            buf = (char)secretStream.Read();
            secretStr.AppendChar(buf);
        }

        // Make the secretStr object read-only.
        secretStr.MakeReadOnly();

        return (secretStr);
    }

To pull the text out of a SecureString object, use the following method:

    public static void ReadSecureString(SecureString secretStr)
    {
        // In order to read back the string, you need to use some special methods.
        IntPtr secretStrPtr = Marshal.SecureStringToBSTR(secretStr);
        string nonSecureStr = Marshal.PtrToStringBSTR(secretStrPtr);

        // Use the unprotected string.
         Console.WriteLine("nonSecureStr = {0}", nonSecureStr);

        Marshal.ZeroFreeBSTR(secretStrPtr);

        if (!secretStr.IsReadOnly())
        {
            secretStr.Clear();
        }
    }

Discussion

A SecureString object is designed specifically to contain string data that you want to keep secret. Some of the data you may want to store in a SecureString object would be a social security number, a credit card number, a PIN number, a password, an employee ID, or any other type of sensitive information.

This string data is automatically encrypted immediately upon being added to the SecureString object, and it is automatically decrypted when the string data is extracted from the SecureString object. The encryption is one of the highlights of using this object.

Another feature of a SecureString object is that when the MakeReadOnly method is called, the SecureString becomes immutable. Any attempt to modify the string data within the read-only SecureString object causes an InvalidOperationException to be thrown. Once a SecureString object is made read-only, it cannot go back to a read/ write state. However, you need to be careful when calling the Copy method on an existing SecureString object. This method will create a new instance of the SecureString object on which it was called, with a copy of its data. However, this new SecureString object is now readable and writable. You should review your code to determine if this new SecureString object should be made read-only similarly to its original SecureString object.

Warning

The SecureString object can be used only on Windows 2000 (with Service Pack 3 or greater) or later operating system.

In this recipe, you create a SecureString object from data read in from a stream. This data could also come from a char* using unsafe code. The SecureString object contains a constructor that accepts a parameter of this type in addition to an integer parameter that takes a length value, which determines the number of characters to pull from the char*.

Getting data out of a SecureString object is not obvious at first glance. There are no methods to return the data contained within a SecureString object. In order to accomplish this, you must use two static methods on the Marshal class. The first is the SecureStringToBSTR, which accepts your SecureString object and returns an IntPtr. This IntPtr is then passed into the PtrToStringBSTR method, also on the Marshal class. The PtrToStringBSTR method then returns an unsecure String object containing your decrypted string data.

Once you are done using the SecureString object, you should call the static ZeroFreeBSTR method on the Marshal class to zero out any memory allocated when extracting the data from the SecureString. As an added safeguard, you should call the Clear method of the SecureString object to zero out the encrypted string from memory. If you have made your SecureString object read-only, you will not be able to call the Clear method to wipe out its data. In this situation, you must either call the Dispose method on the SecureString object (the use of a using block would be preferable here) or rely on the garbage collector to remove the SecureString object and its data from memory.

Notice that when you pull a SecureString object into an unsecure String, its data becomes viewable by a malicious hacker. So it may seem pointless to go through the trouble of using a SecureString when you are just going to convert it into an insecure String. However, by using a SecureString, you narrow the window of opportunity for a malicious hacker to view this data in memory. In addition, some APIs accept a SecureString as a parameter so that you don't have to convert it to an unsecure String. The ProcessStartInfo, for example, accepts a password in its Password property as a SecureString object.

Warning

The SecureString object is not a silver bullet for securing your data. It is, however, another layer of defense you can add to your application.

See Also

The "Secure String Class" topic in the MSDN documentation.

Securing Stream Data

Problem

You want to use the TCP server in the section called "Controlling Access to Types in a Local Assembly" to communicate with the TCP client in the section called "Encrypting/Decrypting a String". However, you need to encrypt the communication and verify that it has not been tampered with in transit.

Solution

Replace the NetworkStream class with the more secure SslStream class on both the client and the server. The code for the more secure TCP client, TCPClient_SSL, is shown in Example 17-15, "TCPClient_SSL class" (changes are in boldface).

Example 17-15. TCPClient_SSL class

class TCPClient_SSL
{
    private TcpClient _client = null;
    private IPAddress _address = IPAddress.Parse("127.0.0.1");
    private int _port = 5;
    private IPEndPoint _endPoint = null;

    public TCPClient_SSL(string address, string port)
    {
        _address = IPAddress.Parse(address);
        _port = Convert.ToInt32(port);
        _endPoint = new IPEndPoint(_address, _port);
    }
    
    public void ConnectToServer(string msg)
    {
        try
        {
            using (client = new TcpClient())
            {
                client.Connect(_endPoint);

         using(SslStreamsslStream =newSslStream(_client.GetStream(),
               false, new RemoteCertificateValidationCallback(
                    CertificateValidationCallback)))
                {
                    sslStream.AuthenticateAsClient("MyTestCert2");

                    // Get the bytes to send for the message.
                    byte[] bytes = Encoding.ASCII.GetBytes(msg);
                    // Send message.
                    Console.WriteLine("Sending message to server: " + msg);
                                      sslStream.Write(bytes, 0, bytes.Length);

                    // Get the response.
                    // Buffer to store the response bytes.
                    bytes = new byte[1024];

                    // Display the response.
                    int bytesRead = sslStream.Read(bytes, 0, bytes.Length);
                    string serverResponse = Encoding.ASCII.GetString(bytes, 0,
                          bytesRead);
                    Console.WriteLine("Server said: " + serverResponse);
                }
            }
        }
        catch (SocketException e)
        {
            Console.WriteLine("There was an error talking to the server: {0}",
            e.ToString());
        }
    }
   private bool CertificateValidationCallback(objectsender,
                X509Certificate certificate, X509Chain chain,
                SslPolicyErrors sslPolicyErrors)
    {
         if (sslPolicyErrors == SslPolicyErrors.None)
         {
             return true;
         }
         else
         {
             if (sslPolicyErrors == SslPolicyErrors.RemoteCertificateChainErrors)
             {
                 Console.WriteLine("The X509Chain.ChainStatus returned an array " +
                    "of X509ChainStatus objects containing error information.");
             }
             else if (sslPolicyErrors ==
                     SslPolicyErrors.RemoteCertificateNameMismatch)
             {
                  Console.WriteLine("There was a mismatch of the name " +
                    "on a certificate.");
             }
             else if (sslPolicyErrors ==
                     SslPolicyErrors.RemoteCertificateNotAvailable)
             {
                  Console.WriteLine("No certificate was available.");
             }
             else
             {
                 Console.WriteLine("SSL Certificate Validation Error!");
             }
         }
         Console.WriteLine(Environment.NewLine +
                          "SSL Certificate Validation Error!");
         Console.WriteLine(sslPolicyErrors.ToString());
         return false;
    }
}

The new code for the more secure TCP server, TCPServer_SSL, is shown in Example 17-16, "TCPServer_SSL class" (changes are in boldface).

Example 17-16. TCPServer_SSL class

class TCPServer_SSL
{
    private TcpListener _listener = null;
    private IPAddress _address = IPAddress.Parse("127.0.0.1");
    private int _port = 55555;

    #region CTORs
    public TCPServer_SSL()
    {
    }

    public TCPServer_SSL (string address, string port)
    {
        _port = Convert.ToInt32(port);
        _address = IPAddress.Parse(address);
    }
    #endregion // CTORs

    #region Properties
    public IPAddress Address
    {
        get { return _address; }
        set { _address = value; }
    }

    public int Port
    {
        get { return _port; }
        set { _port = value; }
    }
    #endregion

    public void Listen()
    {
        try
        {
           _using_(listener = new TcpListener(_address, _port))
            {
                // Fire up the server.
                listener.Start();

                // Enter the listening loop.
                while (true)
                {
                    Console.Write("Looking for someone to talk to... ");

                    // Wait for connection.
                    TcpClient newClient = _listener.AcceptTcpClient();
                    Console.WriteLine("Connected to new client");

                    // Spin a thread to take care of the client.
                    ThreadPool.QueueUserWorkItem(new WaitCallback(ProcessClient),
                                             newClient);
                }
            }
        }
        catch (SocketException e)
        {
            Console.WriteLine("SocketException: {0}", e);
        }
        finally
        {
            // Shut it down.
            _listener.Stop();
        }

        Console.WriteLine("\nHit any key (where is ANYKEY?) to continue...");
        Console.Read();
    }

    private void ProcessClient(object client)
    {
        using (TcpClient newClient = (TcpClient)client)
        {
            // Buffer for reading data.
            byte[] bytes = new byte[1024];
            string clientData = null;

            using (Ssl Stream sslStream = new SslStream(newClient.GetStream()))
             {
                sslStream.AuthenticateAsServer(GetServerCert("MyTestCert2"), false,
                                        SslProtocols.Default, true);

               // Loop to receive all the data sent by the client.
               int bytesRead = 0;
               while ((bytesRead = sslStream.Read(bytes, 0, bytes.Length)) != 0)
               {
                   // Translate data bytes to an ASCII string.
                   clientData = Encoding.ASCII.GetString(bytes, 0, bytesRead);
                   Console.WriteLine("Client says: {0}", clientData);
                   // Thank them for their input.
                   bytes = Encoding.ASCII.GetBytes("Thanks call again!");

                   // Send back a response.
                   ssl Stream.Write(bytes, 0, bytes.Length);
               }
            }
        }
    }

    private static X509Certificate GetServerCert(string subjectName)
     {
         X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
         store.Open(OpenFlags.ReadOnly);
         X509CertificateCollection certificate =
                store.Certificates.Find(X509FindType.FindBySubjectName,
                                        subjectName, true);
          if (certificate.Count > 0)
             return (certificate[0]);
         else
             return (null);
     }
}

Discussion

For more information about the inner workings of the TCP server and client and how to run these applications, see Recipes 17.1 and 17.2. In this recipe, you will cover only the changes needed to convert the TCP server and client to use the SslStream object for secure communication.

The SslStream object uses the SSL protocol to provide a secure encrypted channel on which to send data. However, encryption is just one of the security features built into the SslStream object. Another feature of SslStream is that it detects malicious or even accidental modification to the data. Even though the data is encrypted, it may become modified during transit. To determine if this has occurred, the data is signed with a hash before it is sent. When it is received, the data is rehashed and the two hashes are compared. If both hashes are equivalent, the message arrived intact; if the hashes are not equivalent, then something modified the data during transit.

The SslStream object also has the ability to use client and/or server certificates to authenticate the client and/or the server as well as allowing the client to pass a certificate to the server if the client also needs to prove identity to the server. These certificates are used to prove the identity of the issuer. For example, if a client attaches to a server using SSL, the server must provide a certificate to the client that is used to prove that the server is who it says it is. In order to do this, the certificate must be issued by a trusted authority. All trusted certificates are stored on the client in its root certificate store.

To allow the TCP server and client to communicate successfully, you need to set up an X.509 certificate that will be used to authenticate the TCP server. To do this, you set up a test certificate using the makecert.exe utility. This utility can be found in the <drive>:\Program Files\Microsoft Visual Studio 9.0\Common7\Tools\Bin directory. The syntax for creating a simple certificate is as follows:

    makecert -r -pe -n "CN=MyTestCert2" -e 01/01/2036
            -sr localMachine c:\MyAppTestCert.cer

The options are defined as follows:

  • -r
    The certificate will be self-signed. Self-signed certificates are often created and signed by the developer of a web site in order to facilitate testing of that site before it is moved into production. Self-signed certificates offer no evidence that the site is legitimate.

  • -pe
    The certificate's private key will be exportable so that it can be included in the certificate.

  • -n "CN=MyTestCert2"
    The publisher's certificate name. The name follows the "CN=" text.

  • -e 01/01/2036
    The date at which this certificate expires.

  • -sr localMachine
    The store where this certificate will be located. In this case, it is localMachine. However, you can also specify currentUser (which is the default if this switch is omitted).

The final argument to the makecert.exe utility is the output filename, in this case c:\MyAppTestCert.cer. This will create the certificate in the c:\MyAppTestCert.cer file on the hard drive.

The next step involves opening Windows Explorer and right-clicking on the c:\MyAppTestCert.cer file. This will display a pop-up menu with the Install Certificate menu item. Click this menu item and a wizard will be started to allow you to import this .cer file into the certificate store. The first dialog box of the wizard is shown in Figure 17.1, "The first step of the Certificate Import Wizard". Click the Next button to go to the next step in the wizard.

The next step in the wizard allows you to choose the certificate store in which you want to install your certificate. This dialog is shown in Figure 17.2, "Specifying a certificate store in the Certificate Import Wizard". Keep the defaults and click the Next button.

The final step in the wizard is shown in Figure 17.3, "The last step of the Certificate Import Wizard". On this dialog, click the Finish button.

Figure 17.1. The first step of the Certificate Import Wizard

The first step of the Certificate Import Wizard

After you click the Finish button, the message box shown in Figure 17.4, "The security warning" is displayed, warning you to verify the certificate that you wish to install. Click the Yes button to install the certificate.

Finally, the message box in Figure 17.5, "The import successful message" is displayed, indicating that the import was successful.

At this point, you can run the TCP server and client, and they should communicate successfully.

To use the SslStream in the TCP server project, you need to create a new SslStream object to wrap the TcpClient object:

    SslStream SslStream = new SslStream(newClient.GetStream());

Before you can use this new stream object, you must authenticate the server using the following line of code:

 SslStream.AuthenticateAsServer(GetServerCert("MyTestCert2"),
                               false, SslProtocols.Default, true);

The GetServerCert method finds the server certificate used to authenticate the server. Notice the name passed in to this method; it is the same as the publisher's certificate name switch used with the makecert.exe utility (see the-n switch). This certificate is returned from the GetServerCert method as an X509Certificate object. The next argument to the AuthenticateAsServer method is false, indicating that a client certificate is not required. The SslProtocols.Default argument indicates that the authentication mechanism (SSL 2.0, SSL 3.0, TLS 1.0, or PCT 1.0) is chosen based on what is available to the client and server. The final argument indicates that the certificate will be checked to see whether it has been revoked.

Figure 17.2. Specifying a certificate store in the Certificate Import Wizard

Specifying a certificate store in the Certificate Import Wizard

Figure 17.3. The last step of the Certificate Import Wizard

The last step of the Certificate Import Wizard

Figure 17.4. The security warning

The security warning

Figure 17.5. The import successful message

The import successful message

To use the SslStream in the TCP client project, you create a new SslStream object, a bit differently from how it was created in the TCP server project:

         SslStream SslStream = new SslStream(_client.GetStream(), false,
                  new
    RemoteCertificateValidationCallback(CertificateValidationCallback));

This constructor accepts a stream from the _client field, a false indicating that the stream associated with the _client field will be closed when the Close method of the SslStream object is called, and a delegate that validates the server certificate. The CertificateValidationCallback method is called whenever a server certificate needs to be validated. The server certificate is checked, and any errors are passed into this delegate method to allow you to handle them as you wish.

The AuthenticateAsClient method is called next to authenticate the server:

    SslStream.AuthenticateAsClient("MyTestCert2");

As you can see, with a little extra work, you can replace the current stream type you are using with the SslStream to gain the benefits of the SSL protocol.

See Also

The "SslStream Class" topic in the MSDN documentation.

Encrypting web.config Information

Problem

You need to encrypt data within a web.config file programmatically.

Solution

To encrypt data within a web.config file section, use the following method:

 public static void EncryptWebConfigData(string appPath,
                                            string protectedSection,
                                            string dataProtectionProvider)
    {
        System.Configuration.Configuration webConfig =
                    WebConfigurationManager.OpenWebConfiguration(appPath);
        ConfigurationSection webConfigSection = webConfig.GetSection(protectedSection);

        if (!webConfigSection.SectionInformation.IsProtected)
        {
            webConfigSection.SectionInformation.ProtectSection(dataProtectionProvider);
            webConfig.Save();
        }
    }

To decrypt data within a web.config file section, use the following method:

    public static void DecryptWebConfigData(string appPath, string protectedSection)
    {
        System.Configuration.Configuration webConfig =
                    WebConfigurationManager.OpenWebConfiguration(appPath);
        ConfigurationSection webConfigSection = webConfig.GetSection(protectedSection);

        if (webConfigSection.Section Information.IsProtected)
        {
            webConfigSection.SectionInformation.Un protectSection();
            webConfig.Save();
        }
    }

You will need to add the System.Web and System.Configuration DLLs to your project before this code will compile.

Discussion

To encrypt data, you can call the EncryptWebConfigData method with the following arguments:

   EncryptWebConfigData("/WebApplication1", "appSettings",
                         "DataProtectionConfigurationProvider");

The first argument is the virtual path to the webapplication, the second argument is the section that you want to encrypt, and the last argument is the data protection provider that you want to use to encrypt the data.

The EncryptWebConfigData method uses the virtual path passed into it to open the web.config file. This is done using the OpenWebConfiguration static method of the WebConfigurationManager class:

  System.Configuration.Configuration webConfig =
        WebConfigurationManager.OpenWebConfiguration(appPath);

This method returns a System.Configuration.Configuration object, which you use to get the section of the web.config file that you wish to encrypt. This is accomplished through the GetSection method:

    ConfigurationSection webConfigSection = webConfig.GetSection(protectedSection);

This method returns a ConfigurationSection object that you can use to encrypt the section. This is done through a call to the ProtectSection method:

  webConfigSection.SectionInformation.ProtectSection(dataProtectionProvider);

The dataProtectionProvider argument is a string identifying which data protection provider you want to use to encrypt the section information. The two available providers are DpapiProtectedConfigurationProvider and RsaProtectedConfigurationProvider. The DpapiProtectedConfigurationProvider class makes use of the Data Protection API (DPAPI) to encrypt and decrypt data. The RsaProtectedConfigurationProvider class makes use of the RsaCryptoServiceProvider class in the .NET Framework to encrypt and decrypt data.

The final step to encrypting the section information is to call the Save method of the System.Configuration.Configuration object. This saves the changes to the web.config file. If this method is not called, the encrypted data will not be saved.

To decrypt data within a web.config file, you can call the DecryptWebConfigData method with the following parameters:

 DecryptWebConfigData("/WebApplication1", "appSettings");

The first argument is the virtual path to the webapplication; the second argument is the section that you want to encrypt.

The DecryptWebConfigData method operates very similarly to the EncryptWebConfigData method, except that it calls the UnprotectSection method to decrypt the encrypted data in the web.config file:

 webConfigSection.SectionInformation.UnprotectSection();

If you encrypt data in the web.config file using this technique, the data will automatically be decrypted when the web application accesses the encrypted data in the web.config file.

See Also

The "System.Configuration.Configuration Class" topic in the MSDN documentation.

Obtaining the Full Reason a SecurityException Was Thrown

Problem

You need more information as to why a SecurityException was thrown.

Solution

Use the new properties available on the SecurityException object, as shown in Table 17.2, "SecurityException Properties".

Table 17.2. SecurityException Properties

Property

Description

Action

This property returns a SecurityActione numeration value indicating the cause of the security check failure. Possible values can be any of the following: Assert, Demand, DemandChoice, Deny, InheritanceDemand, InheritanceDemandChoice, LinkDemand, LinkDemandChoice, PermitOnly, RequestMinimum, RequestOptional, RequestRefuseusing

Data

An IDictionary of user-defined key-value pairs.

Demanded

Returns the permission(s) that caused the Demand to fail. The returned object needs to be cast to a Permission, PermissionSet, or PermissionSetCollection type in order to access its information. You can use the is keyword to determine which one of these types this property returned.

DenySetInstance

Returns the denied permission(s) that caused the Demand to fail. This property contains a value whenever a Deny higher up in the stack causes a Demand to fail. The returned object needs to be cast to a Permission, PermissionSet, or PermissionSetCollection type in order to access its information. You can use the is keyword to determine which one of these types this property returned.

FailedAssemblyInfo

Returns an AssemblyName object for the assembly where this exception occurred (i.e., the assembly where the Demand that failed was called).

FirstPermissionThatFailed

Returns an IPermission object of the first permission that failed. This is useful when several permissions in a permission set were demanded at one time. This property identifies which permission caused the exception to occur.

Method

Returns a MethodInfo object for the method where this exception originated. If the cause of the exception was due to a Deny or PermitOnly, the method containing the Deny or PermitOnly will be returned by this property. From this object you can also obtain information on the type and assembly that contain this method.

PermitOnlySetInstance

Returns the permission(s) that were set by a PermitOnly at the point where the security exception was thrown. The returned object needs to be cast to a Permission, PermissionSet, or PermissionSetCollection type in order to access its information. You can use the is keyword to determine which one of these types this property returned.

URL

Returns a string representing the URL of the assembly where this exception originated.

Zone

Returns a SecurityZone enumeration value indicating the zone of the assembly where this exception originated. Possible values can be any of the following:

Internet

Intranet

MyComputer

NoZone

Trusted

Untrusted

Discussion

These new properties on the SecurityException class provide much more insight into what caused the exception to be thrown. For example, if you think a Demand has failed, you can examine the Action property to determine that it was in fact the Demand. Next, you can use the Demanded property to find out exactly what permission(s) the Demand attempted to demand. You can compare this to the GrantedSet property, which contains the permission(s) that were granted to the assembly. Now that you know what caused the Demand to fail, you can use the Method, FailedAssemblyInfo, and URL properties to determine where the failure occurred.

The Data property can be a very useful property to a developer. This property contains key-value pairs that the developer creates and fills with information concerning why this exception occurred. In this property, you can place variable names and the data they contained at the time of the exception. This can give you even more clues as to why this exception was thrown. Be very careful that you do not leak this information out to the user. An attacker can use this information to gain more understanding of your application and overcome its defenses. See the section called "Granting/Revoking Access to a File or Registry Key" for more information on the Exception.Data property.

See Also

The "SecurityException" topic in the MSDN documentation.

Achieving Secure Unicode Encoding

Problem

You want to make sure that your UnicodeEncoding or UTF8Encoding class detects any errors, such as an invalid sequence of bytes.

Solution

Use the constructor for the UnicodeEncoding class that accepts three parameters:

   UnicodeEncoding encoding = new UnicodeEncoding(false, true, true);

Or use the constructor for the UTF8Encoding class that accepts two parameters:

 UTF8Encoding encoding = new UTF8Encoding(true, true);

Discussion

The final argument to both these constructors should be true. This turns on error detection for this class. Error detection will help when an attacker somehow is able to access and modify a Unicode-or a UTF8-encoded stream of characters. If the attacker is not careful, she can invalidate the encoded stream. If error detection is turned on, it will be a first defense in catching these invalid encoded streams.

When error detection is turned on, errors such as the following are dealt with by throwing an ArgumentException:

  • Leftover bytes that do not make up a complete encoded character sequence exist.

  • An invalid encoded start character was detected. For example, a UTF8 character does not fit into one of the following classes: Single-Byte, Double-Byte, Three-Byte, Four-Byte, Five-Byte, or Six-Byte.

  • Extra bits are found after processing an extra byte in a multibyte sequence.

  • The leftover bytes in a sequence could not be used to create a complete character.

  • A high surrogate value is not followed by a low surrogate value.

  • In the case of the GetBytes method, the byte[] that is used to hold the resulting bytes is not large enough.

  • In the case of the GetChars method, the char[] that is used to hold the resulting characters is not large enough.

If you use a constructor other than the one shown in this recipe or if you set the last parameter in this constructor to false, any errors in the encoding sequence are ignored, and no exception is thrown.

See Also

The "UnicodeEncoding Class" and "UTF8Encoding Class" topic in the MSDN documentation.

Obtaining a Safer File Handle

Problem

You want more security when manipulating an unmanaged file handle than a simple IntPtr can provide.

Solution

Use the Microsoft.Win32.SafeHandles.SafeFileHandle object to wrap an existing unmanaged file handle:

  public static void WriteToFileHandle(IntPtr hFile)
    {
        // Wrap our file handle in a safe handle wrapper object.
        using (Microsoft.Win32.SafeHandles.SafeFileHandle safeHFile =
            new Microsoft.Win32.SafeHandles.SafeFileHandle(hFile, true))
        {
            // Open a FileStream object using the passed-in safe file handle.
            using (FileStream fileStream = new FileStream(safeHFile,
                   FileAccess.ReadWrite))
            {
               // Flush before we start to clear any pending unmanaged actions.
               fileStream.Flush();

               // Operate on file here.
               string line = "Using a safe file handle object";

               // Write to the file.
               byte[] bytes = Encoding.ASCII.GetBytes(line);
               fileStream.Write(bytes,0,bytes.Length);
            }
        }
        // Note that the hFile handle is invalid at this point.
    }

The SafeFileHandle constructor takes two arguments. The first is an IntPtr that contains a handle to an unmanaged resource. The second argument is a Boolean value, where true indicates that the handle will always be released during finalization, and false indicates that the safeguards that force the handle to be released during finalization are turned off. Unless you have an extremely good reason to turn off these safeguards, it is recommended that you always set this Boolean value to true.

Discussion

A SafeFileHandle object contains a single handle to an unmanaged file resource. This class has two major benefits over using an IntPtr to store a handle-critical finalization and prevention of handle recycling attacks. The SafeFileHandle is seen by the garbage collector as a critical finalizer, due to the fact that one of the SafeFileHandle's base classes is CriticalFinalizerObject. The garbage collector separates finalizers into two categories: critical and noncritical. The noncritical finalizers are run first, followed by the critical finalizers. If a FileStream's finalizer flushes any data, it can assume that the SafeFileHandle object is still valid, because the SafeFileHandle finalizer is guaranteed to run after the FileStream's.

Tip

The Close method on the FileStream object will also close its underlying SafeFileHandle object.

Since the SafeFileHandle falls under critical finalization, it means that the underlying unmanaged handle is always released (i.e., the SafeFileHandle.ReleaseHandle method is always called), even in situations in which the AppDomain is corrupted and/or shutting down or the thread is being aborted. This will prevent resource handle leaks.

The SafeFileHandle object also helps to prevent handle recycling attacks. The operating system aggressively tries to recycle handles, so it is possible to close one handle and open another soon afterward and get the same value for the new handle. One way an attacker will take advantage of this is by forcing an accessible handle to close on one thread while it is possibly still being used on another in the hope that the handle will be recycled quickly and used as a handle to a new resource, possibly one that the attacker does not have permission to access. If the application still has this original handle and is actively using it, data corruption could be an issue.

Since this class inherits from the SafeHandleZeroOrMinusOneIsInvalid class, a handle value of zero or minus one is considered an invalid handle.

See Also

The "Microsoft.Win32.SafeHandles.SafeFileHandle Class" topic in the MSDN documentation.