[ Team LiB ] |
Recipe 14.8 Securely Storing DataProblemYou 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. SolutionYou 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. DiscussionIsolated 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 AlsoSee the "IsolatedStorageFile Class," "IsolatedStorageStream Class," "About Isolated Storage," and "ComputeHash Method" topics in the MSDN documentation. |
[ Team LiB ] |