DekGenius.com
[ Team LiB ] Previous Section Next Section

Recipe 14.8 Securely Storing Data

Problem

You need to store settings data about individual users for use by your application that is 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 in your data store.

To illustrate how to do this for settings data, we create the following UserSettings class. UserSettings holds only two pieces of information, the user identity (current WindowsIdentity) and the password for our 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 being 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. 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;

Here is the UserSettings class:

// class to hold user settings 
public class UserSettings
{
    IsolatedStorageFile isoStorageFile = null;
    IsolatedStorageFileStream isoFileStream = null;
    XmlDocument settingsDoc = null;
    XmlTextWriter writer = null;
    const string storageName = "SettingsStorage.xml";

    // constructor
    public UserSettings(string password)
    {
        // get the isolated storage
        isoStorageFile = IsolatedStorageFile.GetUserStoreForDomain( );
        // create an internal DOM for settings
        settingsDoc = new XmlDocument( );
        // if no settings, create default
        if(isoStorageFile.GetFileNames(storageName).Length == 0)
        {
            isoFileStream = 
                 new IsolatedStorageFileStream(storageName,
                                               FileMode.Create,
                                               isoStorageFile);

            writer = new XmlTextWriter(isoFileStream,Encoding.UTF8);
            writer.WriteStartDocument( );
            writer.WriteStartElement("Settings");
            writer.WriteStartElement("User");
            // get current user as that is the user
            WindowsIdentity user = WindowsIdentity.GetCurrent( );
            writer.WriteString(user.Name);
            writer.WriteEndElement( );
            writer.WriteStartElement("Password");
            // pass null as the salt to establish one
            string hashedPassword = CreateHashedPassword(password,null);
            writer.WriteString(hashedPassword);
            writer.WriteEndElement( );
            writer.WriteEndElement( );
            writer.WriteEndDocument( );
            writer.Flush( );
            writer.Close( );
            Console.WriteLine("Creating settings for " + user.Name);
        }
        
        // set up access to settings store
        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 performs the creation of the salted and hashed password. The password parameter is the plain text of the password and the existingSalt parameter is the salt to use when creating the salted and hashed version. If no salt exists, like the first time a password is stored, existingSalt should be passed null and a random salt will be generated.

Once we 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 we attempt to validate the password, we 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
        Random  random = new Random( );
        int size = random.Next(16, 64);

        // create salt array
        salt = new byte[size];

        // Use the better random number generator to get
        // bytes for the salt
        RNGCryptoServiceProvider rng = 
                new RNGCryptoServiceProvider( );
        rng.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
    SHA512Managed sha512 = new SHA512Managed( );

    // Get hash of salted password
    byte[] hash = sha512.ComputeHash(saltedPwd);

    // append salt to hash so we have it
    byte[] 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 salted and hashed value, we call CheckPassword and pass in the plain text password to check. First, the stored value is retrieved using the Password property and converted from base64. Then we know we used SHA512, so there are 512 bits in the hash, but we need the byte size so we do the math and get that size in bytes. This allows us to figure out where to get the salt from in the value, so we copy it out of the value and call CreateHashedPassword using that salt and the plain text password parameter. This gives us the hashed value for the password that was passed in to verify, and once we have that, we just compare it to the Password property to see whether we have a match and return true or false appropriately:

    // Check the password against our storage
    public bool CheckPassword(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);
    }
}

The code to use 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.CheckPassword(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 applications to store data that is unique to the application and the user running the application. 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\v1.1\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, we have added the salting and hashing of the password value via the CreateHashedPassword method and verification through the CheckPassword 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 we are storing.

See Also

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

    [ Team LiB ] Previous Section Next Section