What is the point of this post?
There are two desired outcomes of this post:
- Approach confirmation / correction: Someone notices a problem with my approach and gives me an opportunity through suggestions or discussion to fix what I’ve done and make it better.
- Community accepted good practice: My approach is confirmed by the community as a good way to implement an existing best practice, providing a cut-and-paste solution for people looking to implement this on their own.
Assumptions & Background
- The best way to store a user’s password in a database is to hash it with a salt.
- While using a single salt for all passwords is certainly more secure than no salt, it does open you up to people with the same passwords having the same hash.
- You could combat this by using a salt made up of a system salt + the users username (or similar). This creates a problem in systems where the username may change regularly, or where you need to change usernames site-wide for some reason.
- The best way to salt a password is to have a unique salt for each password, and store that salt with the hashed password. While this does take up more storage, it’s generally a relatively tiny amount of storage.
- The salt doesn’t need to be secret, or secure, or difficult to guess, it only has to be relatively unique.
My Approach - Creating the salt
- I decided that the easiest and fastest way to create the salt would be to use the current date and time.
- (If your site is busy enough that a timestamp would not offer enough granularity, consider adding the remote password to the timestamp to add uniqueness.)
- While it is theoretically possible for people to guess the value used for the salt, the salt does not have to be secret, it just has to be unique.
- Additionally, I then convert the timestamp to an MD5 hash (MD5 chosen because it’s cheap and collisions aren’t an issue), which I convert to hex, and use a part of the string (32 characters in my case).
- I do this to get a consistently long string that is unique enough.
This is the function I use to do this:
Public Function MakeSalt(SaltLength As Integer) as String
Dim dDate as New Date
Dim RetVal As String = format(dDate.TotalSeconds, "#")
if SaltLength > 100 Then SaltLength = 100
RetVal = MD5(RetVal)
RetVal = EncodeHex(RetVal)
Retval = Retval.Left(SaltLength)
Return RetVal
End Function
My Approach - Hashing and storing the password
- Once I have the salt, hashing the password is simple using PBKDF2.
- I then convert the hash to hex
- Converting it to hex makes it easier to work with as a string.
- Storing the password as a hex string makes it trivial to reversibly disable a user’s password - add a non-hex string to the beginning or end of the password hash.
- And then combine the hash and password to create the string that gets stored in the database.
- I use a delimiter between the salt and the password hash. This is a convenience only.
- It would be more secure to store the salt and password as a single string without a delimiter, requiring an attacker to know the length of the salt before they could start attacking the password.
- I do not believe this increased security has any real-world benefit - security through obscurity seldom does.
- Specifically, knowing the salt is computationally almost meaningless if they do not know the PBKDF2 iterations, and if they know the iterations they know the hash length and can deduce the salt.
This is the function I use to create the hashed password string for storage:
Public Function PasswordHash(password as String, salt as String) as String
Var RetVal As String
Var cryptIterations As Integer = 712
Var cryptHashLength As Integer = 64
If salt = "" Then
salt = MakeSalt(32)
End If
Var tmpSalt As Text = salt.DefineEncoding(Encodings.UTF8).ToText
Dim saltData As Xojo.Core.MemoryBlock = Xojo.Core.TextEncoding.UTF8.ConvertTextToData(tmpSalt)
Var tmpPassword As Text = password.DefineEncoding(Encodings.UTF8).ToText
Dim passwordData As Xojo.Core.MemoryBlock = Xojo.Core.TextEncoding.UTF8.ConvertTextToData(tmpPassword)
Dim hashData As Xojo.Core.MemoryBlock = Xojo.Crypto.PBKDF2(saltData, passwordData, cryptIterations, cryptHashLength, Xojo.Crypto.HashAlgorithms.SHA256)
RetVal = MemBlockToHex(hashData)
if password = "" Then
RetVal = "Z" + RetVal + "Z"
System.DebugLog("No password given. Corrupting stored password. Good luck logging in!")
end if
RetVal = salt + ":::" + RetVal
Return RetVal
End Function
My Approach - Authentic a user
- Get the username / password from the user
- Get the password string for the username
- Extract the salt from the password string (made up of the salt [+ delimiter] + password hash)
- Turn the provided password into a password string using the stored salt.
- Compare generated password string to stored password string
- A match means a successful auth
This is the function I use for this:
Public Function PasswordCheck(username as String, password As String) as Boolean
Var SQL As String = "SELECT * FROM users_active WHERE username = ?"
Var RS As RowSet = userdb.SelectSQL(SQL, username)
if RS = Nil Or RS.ColumnCount< 1 Then
Return False //No matching username (count < 1), or some other problem (RS = Nil)
Else
Var StoredPasswordString = RS.Column("password")
Var StoredSalt As String = StoredPasswordString.NthField(":::", 1) // I use ::: as a delimiter. You can use StoredPasswordString.Left(salt_length) if you do not use a delimiter.
Var HashedPasswordString As String = PasswordHash(password, StoredSalt) // This function is defined above
if HashedPasswordString = StoredPasswordString Then
Return True // Passwords match, auth SUCCESS
Else
Return False // Passwords do not match, auth FAIL
End If
End Function
Summary
- I use a timestamp converted to hex as a salt.
- I store the salt with the password hash in the database.
- Authenticating is as simple as reading the stored password, extracting the salt, hashing the offered password, and comparing to see if they match.
Suggestions or corrections, please…