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.
|