Back in March, I wrote about securely implementing a persistent login system for your website or web app. That only helps you if you actually have a user authentication system in place, and such a system is very easy to do wrong. Just ask LinkedIn or Sony.
Even if your service does not store credit cards, or anything sensitive for that matter, it is still your responsibility to take great care of your user's passwords. Let's face facts, users will reuse passwords. That means if a user creates an account on your system with the same credentials as their Gmail account, an attacker will gain access to a whole lot of that user's life should your database fall into the wrong hands. It doesn't really matter that it was the user's fault for reusing the password, you'll be the one blamed.
We've all been to websites that require passwords to contain at least one letter, one number, and one symbol. This is a method of shifting password security from the website operator to the user. But users will not, as a whole, choose secure passwords. They'll continue to choose passwords such as "1Nsecure" and "NCC-1701". The solution is to implement a system that can keep even weak passwords secure. Ensuring users cannot choose any of the 10,000 most common passwords will go a very long way too.
I recommend a technique called Security Through Obesity. The general idea is to separate the two parts of the user credentials (the username and the password) so that an attacker who gains access to your database cannot easily determine which password belongs to which user.
You're going to need two tables. We'll use Users and Passwords for this example.
The Users table needs at least 5 columns in addition to others for your own use: UserID INTEGER PRIMARY KEY, Username VARCHAR(48) NOT NULL, Salt1 VARCHAR(128) NOT NULL, Hash2 VARCHAR(128) NOT NULL, and PasswordVersion INTEGER NOT NULL. The Username column must be case-insensitive unique, not plain unique so that ThomMcGrath will equate to thommcgrath. You can do this by simply storing every username as lowercase, or I prefer to use a lowercase index in PostgreSQL. Either way, you'll need an index on Username. The Salt1 and Hash2 columns must also be unique, but there is no need for case-insensitivity. Although not a requirement, avoid using the username as the primary key, as I demonstrated here. If you choose to allow it, this will let the user change their username without breaking your relationships. This is just a little future proofing.
The Passwords table needs 2 columns: Hash1 VARCHAR(128) NOT NULL, and Salt2 VARCHAR(128) NOT NULL. You should also create a non-unique index on Hash1. If your database engine requires a primary key, just add an autoincrement integer. Do not use Hash1 or Salt2 as the primary key.
Creating a user
I'm going to assume you've securely collected a username and password for a user you'd like to store. I'm also going to assume you've verified the username is not already in use.
First, we need to create two salts. In Xojo 2013r4, use Crypto.GenerateRandomBytes(64) to generate 64 bytes of random data. This function is a Cryptographically Secure Pseudorandom Random Number Generator or CSPRNG for short. Do not use the Rnd function or Random class for generating salts, as they can be predictable and will reduce the effectiveness of your storage. You'll need to run this function twice to create your two salts.
Next you need to generate two hashes from these salts. You need to choose a hashing algorithm and an iteration count, because we're going to use PBKDF2. This is an excellent hashing technique designed to be slow and thwart the use of parallel processing, making a hacker's job harder. The greater the iteration count, the greater CPU resources will be required to compute the hash. But remember, since we are creating two hashes, the time required will actually be doubled. As for the hashing algorithm, technically any hashing algorithm will suffice for PBKDF2, but I recommend SHA-512.
Let's briefly talk about the PasswordVersion value. This is an internal identifier you can use to tell your code exactly how to compute the matching hashes. You should put a constant somewhere with your current version. Don't get fancy, just use incrementing integers: 1, 2, 3, etc. In our example, I'm just going to assume you have a global constant called PasswordVersion whose value is 1.
I recommend using a function to convert a PasswordVersion variable into the algorithm and iteration count. It will be more obvious why later. Such a function would look something like this:
Sub TranslatePasswordVersion (Version As Integer, ByRef Iterations As Integer, ByRef Algorithm As Crypto.Algorithm, ByRef ByteCount As Integer)
Select Case Version
Case 1
Iterations = 1000
Algorithm = Crypto.Algorithm.SHA512
ByteCount = 64
End Select
End Sub
Your code up to this point might look something like this:
Dim Iterations, ByteCount As Integer
Dim Algorithm As Crypto.Algorithm
TranslatePasswordVersion(PasswordVersion, Iterations, Algorithm, ByteCount)
Dim Salt1 As MemoryBlock = Crypto.GenerateRandomBytes(ByteCount)
Dim Salt2 As MemoryBlock = Crypto.GenerateRandomBytes(ByteCount)
Dim Hash1 As MemoryBlock = Crypto.PBKDF2(Salt1, Password, Iterations, ByteCount, Algorithm)
Dim Hash2 As MemoryBlock = Crypto.PBKDF2(Salt2, Password, Iterations, ByteCount, Algorithm)
Now you're ready to store your values in the database. I'm going to use PostgreSQL prepared statements for this, but if you're familiar with databases, you should be able to adapt the code pretty readily.
Dim UsersInsert As PostgreSQLPreparedStatement = MyDatabase.Prepare("INSERT INTO Users (Username, Salt1, Hash2, PasswordVersion) VALUES ($1, $2, $3, $4);")
Dim PasswordsInsert As PostgreSQLPreparedStatement = MyDatabase.Prepare("INSERT INTO Passwords (Hash1, Salt2) VALUES ($1, $2);")
MyDatabase.SQLExecute("BEGIN TRANSACTION;")
UsersInsert.SQLExecute(Username, EncodeHex(Salt1), EncodeHex(Hash2), PasswordVersion)
PasswordsInsert.SQLExecute(EncodeHex(Hash1), EncodeHex(Salt2))
Dim Picker As New Random
Picker.RandomizeSeed()
Dim FakeCount As Integer = Picker.InRange(8,12)
For I As Integer = 1 To FakeCount
PasswordsInsert.SQLExecute(EncodeHex(Crypto.GenerateRandomBytes(ByteCount)), EncodeHex(Crypto.GenerateRandomBytes(ByteCount)))
Next
MyDatabase.SQLExecute("COMMIT TRANSACTION;")
When inserting your password rows, you also need to insert a random number of fake rows. These are designed to waste the time of an attacker, as there is no way for them to tell the difference between a legit and fake password row. You insert a random number of them to ensure the attacker cannot simply skip every 4 rows, for example.
Validating a user
Once you've securely collected the username and password from the user, you will query the Users table to collect the Salt1, Hash2, and PasswordVersion columns. Use Salt1 to compute a hash for the password, then look for that hash in the Passwords table. If the hash is found, use the matching Salt2 to compute another hash and ensure it matches Hash2 from the Users table. This is to ensure an attacker cannot insert a second password for a user. It guarantees that a user can have one and only one password and also guards against the extremely unlikely possibility of the same Hash1 appearing twice in the Passwords table.
In the example code, Results is a RecordSet containing your query results.
Dim Salt1 As MemoryBlock = DecodeHex(Results.Field("Salt1").StringValue)
Dim Hash2 As MemoryBlock = DecodeHex(Results.Field("Hash2").StringValue)
Dim UserPasswordVersion As Integer = Results.Field("PasswordVersion").IntegerValue
Dim Iterations, ByteCount As Integer
Dim Algorithm As Crypto.Algorithm
TranslatePasswordVersion(UserPasswordVersion, Iterations, Algorithm, ByteCount)
Dim Hash1 As MemoryBlock = Crypto.PBKDF2(Salt1, Password, Iterations, ByteCount, Algorithm)
Dim PasswordSelect As PostgreSQLPreparedStatement = MyDatabase.Prepare("SELECT Salt2 FROM Passwords WHERE Hash1 = $1;")
Results = PasswordSelect.SQLSelect(Hash1)
If Results = Nil Then
Return False
End If
While Not Results.EOF
Dim Salt2 As MemoryBlock = DecodeHex(Results.Field("Salt2").StringValue)
Dim Hash2_Computed As MemoryBlock = Crypto.PBKDF2(Salt2, Password, Iterations, ByteCount, Algorithm)
If Hash2.StringValue(0,Hash2.Size) = Hash2_Computed.StringValue(0,Hash2_Computed.Size) Then
If UserPasswordVersion <> PasswordVersion Then
ChangeUserPassword(Username, Password)
End If
Return True
End If
Results.MoveNext
Wend
Return False
This code assumes you have a function called ChangeUserPassword that will execute nearly the same code as the initial user creation. The purpose of this is to automatically upgrade the password version when you've changed it.
Be aware that using a password version integer is not backwards compatible. Software using version 2 will be able to understand version 1 passwords and upgrade them to version 2, but version 1 will not be able to understand version 2 passwords. If this is a problem for you, consider storing the iteration count on the user row itself, instead of the password version. It'll make an attacker's life just a tiny bit easier, but it will be backwards and forwards compatible.
Some more details on password changes
In your ChangeUserPassword function, generate new salts and hashes. Do not reuse salts. Also don't try to ever delete rows from the passwords table. Your function should just replace Salt1, Hash2, and PasswordVersion with new values, insert a new legit row into the Passwords table, and insert a random number of bogus rows. Nothing more, and just as importantly, nothing less.
More password storage tricks
You can improve your security even further if you have the infrastructure. It is perfectly acceptable to have your Passwords table in a separate database. That database can even exist on a separate machine, or even be a different database type entirely. Imagine a MySQL or PostgreSQL database for your user data, and a local SQLite database for your passwords.
There's also no reason why one password database must only be used for one site. If you run multiple sites, you can store all passwords for all sites - even if the sites are completely unrelated - in the same passwords database. Remember, you want as much data in your password database as possible. The source article mentions seeding the database with 1TB of bogus data. The more the better.
Finishing up
Now you can create a user and validate their credentials. Passwords are stored securely, have no maximum length, and even weak passwords will be nice and cozy in your database. Of course, weak passwords will not stop a brute force attack on your login page but a firewall or simple program logic should be implemented to mitigate those kinds of attacks. Besides, this mechanism is designed to protect passwords in the case your database is attacked. It will not help prevent individual account hijacking. For that, you'll need something like two-factor authentication.
This article was written with input by Kem Tekinay. Kem will be speaking about Security via Obesity at the 2014 Xojo Developer Conference in Las Vegas this March.