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.
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.
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 SDKv1.1Bin
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.