Wednesday, June 18, 2008

Storing Passwords Securely with Salt

In a previous article on Securely Storing Passwords I discussed encrypting passwords before storing them in the database. This provides an additional level of security since it prevents users from viewing passwords. However, there are still potential security problems with this mechanism. If someone got a copy of the database and the hash key they could run queries against the database to discover passwords. This could be done by hashing common passwords and using these results to query the database. All the usernames for a password that matched would be returned potentially compromising many user accounts.

The use of a Salt with the password can make the passwords less vulnerable to these types of attacks. Basically you add an additional string to the password before doing the hash. Ideally this string would be random and unique for every user, ensuring that every user has a unique hash value even if they choose the same password as another user.

I like to use GUIDs as unique identifiers which gives me a strong unique value. Another benefit of using GUIDs is that I have a long string value which I can use to salt the passwords entered by the user. Since I am already storing this value as my primary key I do not have to add another column to my table or come up with an algorithm to generate random text. To demonstrate this I’ll start with a simple membership table outlined below:

Column Data Type Notes
UserID Char(36) Primary Key GUID
UserName CiChar(100) User Name with Unique Index
Password Char(30) Hashed password and Salt
UserEmail CiChar(255) Users e-mail address
IsLockedOut Logical Flag to lock-out user
LastLogin Timestamp Date/Time of last successful login

To encode our password for storage or to validate it first we need to add the salt to the password. Once the proper salted password is established we encode it with our SHA-1 algorithm.

   1: private string EncodePassword(string password, string salt)
   2: {
   3:     string saltedPassword = password + salt;
   4:     
   5:     // encode the password using SHA-1
   6:     HMACSHA1 hash = new HMACSHA1();
   7:     hash.Key = HexToByte(ValidationKey);
   8:     // note: the encoded value is a 28 character base64 string
   9:     return Convert.ToBase64String(hash.ComputeHash(
  10:            Encoding.Unicode.GetBytes(saltedPassword)));
  11: }

First let’s create a function to add a user to the table. We will use the encode password function to ensure that the password is stored in its encoded form.

   1: public bool CreateUser(string username, string password, string email)
   2: {
   3:     AdsConnection conn = new AdsConnection(connectionString);  
   4:     
   5:     // Check to see if the user already exists   
   6:     AdsCommand cmd = new AdsCommand("SELECT COUNT(*) FROM Membership " +   
   7:         "WHERE Username = :username", conn);  
   8:     cmd.Parameters.Add("Username",   
   9:         System.Data.DbType.String).Value = username;  
  10:     
  11:     try
  12:     {
  13:         int UserCount = cmd.ExecuteScalar();
  14:         
  15:         if (UserCount != 0)
  16:             return false;
  17:     }
  18:     catch (AdsException e)
  19:     {
  20:         // Handle Advantage errors here  
  21:         return false;
  22:     }
  23:     finally
  24:     {
  25:         conn.Close();
  26:     }
  27:     
  28:     string uID = Guid.NewGuid().ToString();
  29:     
  30:     cmd.CommandText = "INSERT INTO Membership " +
  31:         " (UserID, Username, Password, UserEmail, IsLockedOut" +
  32:         " Values(:UserID, :Username, :Password, :Email, true)";
  33:     
  34:     cmd.Parameters.Add("UserID", System.Data.DbType.String).Value = uID;
  35:     cmd.Parameters.Add("Username", System.Data.DbType.String).Value = username;
  36:     cmd.Parameters.Add("Password", System.Data.DbType.String).Value = 
  37:                        EncodePassword(password, uID);
  38:     cmd.Parameters.Add("Email", System.Data.DbType.String).Value = email;
  39:     
  40:     try
  41:     {
  42:         int recAdded = cmd.ExecuteNonQuery();
  43:         
  44:         if (recAdded > 0)
  45:             return true;
  46:         else
  47:             return false;
  48:     }
  49:     catch (AdsException e)
  50:     {
  51:         // Handle Advantage errors here
  52:         return false;
  53:     }
  54:     finally
  55:     {
  56:         conn.Close();
  57:     }
  58:     
  59:     return true;
  60: } 

Now that we have our new EncodePassword function we can create a function to validate the user. In this case we will need to return a resultset containing the user information. We need this to verify that the user is not locked out and we will be using the UserID as our salt value.

   1: public bool ValidateUser(string username, string password)
   2: {
   3:     bool isValid = false;
   4:     AdsConnection conn = new AdsConnection(connectionString);
   5:     AdsCommand cmd = new AdsCommand("SELECT UserID, Password, IsLockedOut " +
   6:         "FROM Membership WHERE Username = :Username", conn);
   7:     
   8:     cmd.Parameters.Add("Username", System.Data.DbType.String).Value = username;
   9:     
  10:     AdsDataReader reader = null;
  11:     bool isLockedOut = false;
  12:     string pwd = "";
  13:     string salt = "";
  14:     
  15:     try 
  16:     { 
  17:         conn.Open();
  18:         reader = cmd.ExecuteReader();
  19:         
  20:         if (reader.HasRows)
  21:         {
  22:             reader.Read();
  23:             salt = reader.GetString(0).Trim();
  24:             pwd = reader.GetString(1).Trim();
  25:             isLockedOut = reader.GetBoolean(2);
  26:         }
  27:         else
  28:         {
  29:             // User not found so return false
  30:             return false;
  31:         }
  32:         reader.Close();
  33:         // if user is locked out so return false
  34:         if (isLockedOut == true)
  35:             return false;
  36:         
  37:         // Check the password
  38:         if (pwd == EncodePassword(password, salt))
  39:         {
  40:             // Password is valid
  41:             isValid = true;
  42:             // Update the LastLogin field
  43:             AdsCommand updateCmd = new AdsCommand("UPDATE Membership SET " +
  44:                 "LastLogin = NOW() WHERE Username = :UserName", conn);
  45:             updateCmd.Parameters.Add("Username",
  46:                 System.Data.DbType.String).Value = username;
  47:             updateCmd.ExecuteNonQuery();
  48:         }
  49:         else
  50:         {
  51:             // Password is not valid
  52:             isValid = false;
  53:         }
  54:     }  
  55:     catch (AdsException e)
  56:     {
  57:         // Handle any Advantage errors here
  58:     }
  59:     finally
  60:     {
  61:         if (reader != null) { reader.Close();}
  62:         conn.Close();
  63:     }
  64:     
  65:     return isValid;
  66: }

In this case we simply return false unless the username and password are correct. This function could easily be modified to return an integer value which could provide additional information. For example: 0 = Success, 1 = Invalid password, 2 = Invalid username, 3 = User locked out. You could even create an enum for these return values.

I recommend reading “Could you Pass the Salt?” by Scott Mitchell and Thomas Tomiczek which describes the perils of a hashed password without “salting”.

UPDATE: When I looked back through the code for this post I realized that I used another helper function to convert the validationKey,a hexidecimal number from 16 to 48 characters in length, to a byte array.  I posted the HexToByte function below.

   1: private byte[] HexToByte(string hexString)
   2: {
   3:     byte[] returnBytes = new byte[hexString.Length / 2];
   4:     for (int i = 0; i < returnBytes.Length; i++)
   5:         returnBytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
   6:     return returnBytes;
   7: }

2 comments:

Joe White said...

"Then we must limit the length of the salted password to 50 characters, the length of our password field. Once the proper salted password is established we encode it with our SHA-1 algorithm."

That doesn't make any sense. You don't need to limit the length of the thing you're hashing -- all hashes are the exact same length. (And in your sample code, you don't limit it.)

Chris said...

Joe,

You are absolutely right. I added the comment about the hash length (28 chars) in the code sample but I forgot to remove the line about limiting the length.

I based this example on the Sample Membership Provider from Microsoft. Which has options to encrypt the password instead of hashing; in that case the salted password length would need to be limited. For the example here I chose to just use the hash algorithm so limiting the length is unnecessary.

Thanks for pointing this out.