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:
"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.)
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.
Post a Comment