Friday, June 13, 2008

Securely Storing Passwords

Passwords are a necessary evil in today’s digital world. If you run a Web site where you allow users to order products or to access exclusive content chances are you have them log in so you know who they are. While many technologies such as the ASP.NET 2.0 Login Controls make managing passwords much easier the passwords still must be stored somewhere. The way you store this critical piece of information is very important.

There are several levels of security that can and should be used.

  1. Physical security of the server should be maintained
  2. Access to tables with sensitive information should be controlled through permissions.
  3. Tables should be encrypted ensuring that the table cannot be read.
  4. Highly sensitive fields should be additionally encrypted

Although each of these security mechanisms are important the first three are simple to implement. Advantage includes features for restricting access to tables through permissions. Additionally encryption of tables, memos and indexes is fully supported. Encrypting of individual fields is a bit more complex since we want the data to always be in an encrypted form so even if someone circumvented the first three security measures the data in the encrypted field would not be compromised.

The password must be stored somehow so we can validate users. However, there is really no need for a human to be able to read these passwords. The code should be able to manage the passwords in their encrypted form. There are many choices for encrypting these passwords and I won’t attempt to cover them all here.

A couple of options include encoding the password and storing it in the table. The password could then be decoded and validated. This method is somewhat secure and allows for simple password recovery since we can decode the password and provide it to the proper user. As long as your encryption key is kept safe the passwords will be secure.

Another method is to do a one-way hash of the password, generating a fixed-length binary value from the password which cannot be decrypted. There are several hash algorithms which provide this functionality including MD4, MD5 and SHA-1. The SHA-1 algorithm is widely used by the Internet developer community and many tools are available for encrypting strings using this algorithm.

The .NET Framework includes many security libraries one of these is the System.Security.Cryptography library which includes the HMACSHA1 class. In order to encode our password we first set the Key property which will be used to encode our password. Next we use the ComputeHash function to encode the password. ConputeHash accepts a byte array and returns an encoded byte array. In the example we will convert this byte array into a Base64 encoded string to be stored in the table.

   1: private string EncodePassword(string Password)
   2: {
   3:     HMACSHA1 hash = new HMACSHA1();
   4:     hash.Key = HexToByte(ValidationKey);
   5:     return Convert.ToBase64String(hash.ComputeHash(Encoding.Unicode.GetBytes(password)));
   6: }

To validate the password we encode the user provided password and then compare the encoded value to the value stored in the table. In this case we are using a table named Membership which contains a Username and Password field. The table should have a unique index on the Username field to ensure that every user has a unique username.

   1: public override bool ValidateUser(string username, string password)
   2: {
   3:     bool bValid = false;
   4:     string sPass = EncodePassword(password);
   6:     AdsConnection conn = new AdsConnection(connectionString);
   7:     AdsCommand cmd = new AdsCommand("SELECT COUNT(*) FROM Membership " +
   8:             " WHERE Username = :Username AND Password = :Password", conn);
  10:     cmd.Parameters.Add("Username", System.Data.DbType.String).Value = username;
  11:     cmd.Parameters.Add("Password", System.Data.DbType.String).Value = sPass;
  13:     try
  14:     {
  15:         conn.Open();
  17:         // Get the count of users who match the username and password
  18:         int iValid = cmd.ExecuteScalar();
  20:         // Valid if only one and only one record matches the criteria
  21:         bValid = iValid == 1;
  22:     }
  23:     catch (AdsException e)
  24:     {
  25:         // Log or handle exceptions here
  26:     }
  27:     finally
  28:     {
  29:         // close the connection
  30:         conn.Close();
  31:     }
  33:     return bValid;
  34: }

In a production environment you may want to have additional criteria for validating a user. For example you may want to approve users before they are allowed to log into your site. You may also want to have the ability to lock users out of the system. Additionally you could log both successful and unsuccessful login attempts and lock out a user if multiple invalid logins are attempted.

No comments: